Skip to content

Feature/licensing architecture#30

Open
magmacomputing wants to merge 7 commits into
mainfrom
feature/licensing-architecture
Open

Feature/licensing architecture#30
magmacomputing wants to merge 7 commits into
mainfrom
feature/licensing-architecture

Conversation

@magmacomputing
Copy link
Copy Markdown
Owner

@magmacomputing magmacomputing commented May 15, 2026

First draft of the Licensing Strategy to protect Premium Term Plugins

Summary by CodeRabbit

  • New Features

    • Added JWT-based licensing engine with revocation checks and domain locking
    • Added decodeJWT and base64Encode utility functions
    • Added Tempo.license API for license status inspection
    • Added premium plugin metadata support and collision detection on term registration
  • Bug Fixes

    • Fixed parsing state leakage and side-effects during normalization
    • Improved fractional precision for 9–10 digit epoch timestamps
  • Documentation

    • Added comprehensive licensing architecture and strategy documentation

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

📝 Walkthrough

Walkthrough

This PR introduces a licensing system for Tempo plugins with JWT-based key verification and cascading discovery, publishes the shared library as a public npm package, and refactors Tempo's internal architecture for type safety. It spans version 2.9.3 → 2.10.0 across the monorepo and includes comprehensive documentation, tests, and build pipeline integration.

Changes

Licensing Infrastructure & Core Integration

Layer / File(s) Summary
Release & Package Management
package.json, CHANGELOG.md, .gitignore, packages/library/LICENSE
Root and library package versions bumped to 2.10.0, devDependencies updated, MIT license file added for library, secrets/credentials section added to gitignore, and changelog entries documenting new licensing features.
Library as Public NPM Package
packages/library/package.json, packages/library/src/tsconfig.json, packages/library/test/tsconfig.json, packages/library/tsconfig.json, tsconfig.base.json
Library package.json publishConfig set to public access, TypeScript ESNext.Temporal library added to compilation, and internal #tempo path aliases removed from all library TypeScript configurations to decouple the published package from Tempo internals.
Licensing Type Contracts & Enums
packages/tempo/src/support/support.enum.ts, packages/tempo/src/support/support.index.ts, packages/tempo/src/support/support.symbol.ts, packages/tempo/src/plugin/term/term.type.ts, packages/tempo/src/tempo.type.ts
New LICENSE enum with validation states (None, Pending, Active, Expired, Revoked, Invalid) exported from support module, new $License symbol registered in Tempo's symbol registry, new PremiumPlugin interface with optional status/expires/updated metadata, and new Internal licensing interfaces (LicenseScope, ValidationResult, LicensingModule, LicenseState) added to tempo.type exports.
JWT Decoding & Crypto Utilities
packages/library/src/common/utility.library.ts, packages/tempo/src/library.index.ts
Exported decodeJWT utility for fast unverified JWT payload extraction with cross-runtime support (atob/Buffer), exported base64Encode with Buffer/btoa fallback, Deno runtime detection in getContext, and Cipher class re-exported from library.
License Discovery & Optimistic Initialization
packages/tempo/src/support/support.init.ts, packages/tempo/src/support/support.default.ts
Discovery cascade implemented during init (options → globalThis.TEMPO_DISCOVERY → storage → globalThis variable), setLicense() helper decodes JWT claims into optimistic runtime.license state and kicks off async Pledge-based verification, extendState() updated to handle sphere and license options with global-only enforcement.
Runtime License State & Singleton Reset
packages/tempo/src/support/support.runtime.ts, packages/library/src/common/storage.library.ts
TempoRuntime.license field added tracking status/scopes/metadata/error, getRuntime() refactored to use multi-line conditionals, resetRuntimeForTesting() function added for test isolation, and Deno storage access updated to use context.global.Deno.env.get().
Tempo Core Licensing API & Term Enforcement
packages/tempo/src/tempo.class.ts
Tempo.license static getter exposes runtime license state, Tempo.terms returns PremiumPlugin array with merged licensing metadata, term collision detection throws fatal error on key/scope naming conflicts, lazy-term resolver blocks access for Revoked/Expired/Invalid scopes via #isBlocked() guard, Licensing Reckoning async flow during init resolves pending JWS pledge, userProvidedKeys tracking added, and extensive refactoring of internal methods replacing casted (this as any) patterns with typed this[$Internal]() dispatch throughout init/extend/discovery/getter paths.
Build Configuration & Module Alias Routing
packages/tempo/package.json, packages/tempo/rollup.config.js, packages/tempo/src/plugin/plugin.util.ts, packages/tempo/src/plugin/term/term.index.ts, packages/tempo/src/plugin/term/term.util.ts, packages/tempo/src/module/module.parse.ts, packages/tempo/src/tsconfig.json, packages/tempo/test/tsconfig.json, packages/tempo/vitest.config.ts
Tempo package.json adds #tempo/license import specifier, build script verifies dist/lic/index.js existence, prepublishOnly validates TEMPO_LICENSE_PATH environment variable, Rollup alias plugin routes #tempo/license to configurable licensePath with ESM entryFileNames redirecting licensing chunks to lic/ with obfuscation, plugin utility functions converted from const-arrow to function declarations, term barrel removes immediate side effects, import paths adjusted to relative paths, parse engine updates epoch detection thresholds, TypeScript path aliases added for #tempo/license across src/test/base configs, and Vitest configured with TEMPO_LICENSE_PATH override support.
Default No-Op License Module & Type Resolution
packages/tempo/src/support/support.license.ts, packages/tempo/bin/resolve-types.ts
support.license.ts provides public-repo Validator class with no-op async verify() returning active community license state, and resolve-types.ts enhanced to copy licensing .d.ts files to dist/lic/ and rewrite #tempo/license aliases in generated type definitions.
Comprehensive Licensing Test Suite
packages/tempo/test/plugins/licensing.full.test.ts
Vitest tests cover optimistic JWT hydration into runtime.license, pledge establishment, invalid token handling, global state sharing across instances, discovery from globalThis.TEMPO_LICENSE, full reckoning transitions (Pending→Active/Revoked), error message propagation, and eager-discovery guard blocking revoked premium scopes.
Licensing Architecture & Strategy Documentation
packages/tempo/plan/licensing_architecture.md, packages/tempo/plan/licensing_strategy.md, packages/tempo/plan/v3-architecture-discoverability.md, packages/tempo/doc/releases/v2.x.md
Licensing Architecture Plan documents "No-PAT" public npm distribution, JWT activation semantics (aud/exp/jti), 7-day grace period, signed JWS revocation with fail-open behavior, and license wallet discovery/persistence. Licensing Strategy details Validator/Gatekeeper/Plugin roles, optimistic sync with background verification, discovery cascade order, and scenario-by-scenario UX. V3 Architecture Plan outlines publishing library as independent package with ESM deduplication and IIFE script-tag support. Release notes document new licensing features, bug fixes, and documentation updates for v2.10.0.

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

  • magmacomputing/magma#18: Both PRs modify parsing logic in packages/tempo/src/module/module.parse.ts for epoch/long-form input detection, with this PR updating isLong determination thresholds.
  • magmacomputing/magma#16: Both PRs directly update ParseEngine.parseLayout in the same file with changes to length-based unit detection logic.
  • magmacomputing/magma#27: Both PRs modify pattern compiler integration and path alias configuration in TypeScript and Rollup configs.
  • magmacomputing/magma#17: Both PRs build on TempoRuntime singleton architecture with new resetRuntimeForTesting() and $License symbol added to this PR's licensing state foundation.

🐰 A licensing tale we weave today,
JWT tokens guard the plugin way.
With cascading keys and scopes that gleam,
The library published—a monorepo dream!
TypeScript types now safely cast,
No more (this as any)—at last! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature/licensing architecture' directly describes the main change: implementing a licensing architecture framework for protecting premium term plugins. It aligns with the extensive licensing infrastructure, JWT validation, license state management, and plugin licensing metadata added throughout the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/licensing-architecture

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/tempo/src/tempo.class.ts (1)

1338-1354: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

License enforcement is bypassed on eager term discovery.

The premium-term guard exists in delegator callback path, but #discover() eagerly installs getters for all terms without that check. In non-lazy mode, premium terms can still resolve even when status is Revoked/Expired/Invalid (Line 1338 onward).

Suggested fix
 		} else {
 			Tempo.#terms.forEach(term => {
 				const define = (keyOnly: boolean, anchor?: any) => {
+					const rt = getRuntime();
+					const licensedOut = [LICENSE.Revoked, LICENSE.Expired, LICENSE.Invalid].includes(rt.license.status as any);
+					if (licensedOut && hasOwn(rt.license.scopes, term.key)) return undefined;
 					try {
 						const res = term.resolve ? term.resolve.call(this, anchor) : term.define.call(this, keyOnly, anchor);
 						const out = (getTermRange(this, (Array.isArray(res) ? (res as any) : [res]), keyOnly, anchor) as any);
 						return isObject(out) ? secure(out) : out;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tempo/src/tempo.class.ts` around lines 1338 - 1354, Tempo.#terms are
being eagerly exposed by the define closure and this.#setLazy without enforcing
the premium-term/license check used elsewhere; update the define installation so
it first checks the same premium-term guard used in the delegator callback path
before invoking term.resolve or term.define, returning a license-denied
placeholder or throwing as the delegator does when status is
Revoked/Expired/Invalid; ensure the check is applied for both the primary key
(where this.#setLazy is called with true) and the optional term.scope (false),
and keep references to term.resolve, term.define and this.toDateTime() intact so
only the pre-check is added rather than changing resolution logic.
🧹 Nitpick comments (2)
packages/tempo/rollup.config.js (1)

11-11: ⚡ Quick win

Build order dependency on external package.

The licensePath resolves to ../../../tempo-plugin/packages/_core/dist/index.js, which requires the _core package to be built before the Tempo bundle step runs. Ensure your build pipeline builds tempo-plugin/_core first, or this will fail with a module resolution error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tempo/rollup.config.js` at line 11, The rollup config's licensePath
constant (licensePath in packages/tempo/rollup.config.js) points to
../../../tempo-plugin/packages/_core/dist/index.js which introduces a
build-order dependency; ensure the tempo-plugin/_core package is built before
packaging Tempo by updating the build pipeline (CI task order or npm/yarn
workspace scripts) to run the _core build prior to the Tempo bundle step, or
change the reference to a source-level import pattern that doesn't require the
compiled dist (e.g., point to the package entry or use the workspace package
name) so the module resolution succeeds when running rollup.
packages/tempo/test/plugins/licensing.full.test.ts (1)

10-10: ⚡ Quick win

Avoid type cast; use proper reset method.

Bypassing TypeScript safety with (rt as any).state hides potential issues. If runtime state needs to be reset for testing, expose a proper resetState() test utility or make the property legally deletable.

♻️ Proposed safer alternative

Add a test-only reset helper to the runtime module:

// In support.runtime.ts (test exports section)
export function resetRuntimeForTesting() {
  const rt = getRuntime();
  delete (rt as any).state;  // Encapsulated here
}

Then in the test:

+import { getRuntime, resetRuntimeForTesting } from '#tempo/support/support.runtime.js';
+
 beforeEach(() => {
-  const rt = getRuntime();
-  delete (rt as any).state;
+  resetRuntimeForTesting();
   vi.clearAllMocks();
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tempo/test/plugins/licensing.full.test.ts` at line 10, The test
currently bypasses TypeScript safety by using delete (rt as any).state; instead
add a proper test-reset helper in the runtime module (e.g., implement and export
resetRuntimeForTesting() in support.runtime.ts) that obtains the runtime via
getRuntime() and performs the controlled state deletion/reset internally, then
update the test in licensing.full.test.ts to call resetRuntimeForTesting()
rather than deleting (rt as any).state; this keeps the delete encapsulated,
preserves type safety at call sites, and exposes a clear API for test resets
(reference symbols: getRuntime, resetRuntimeForTesting, and the place where the
test currently deletes rt.state).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.gitignore:
- Around line 56-57: The .gitignore entries are malformed: replace the lines
that currently read ".npmrc (global/local if they contain tokens)" and "! .npmrc
(the one we created is safe as it uses variables)" with a proper ignore rule and
negation using the correct gitignore syntax — e.g. add a rule to ignore
tokenized/local npmrc files (such as ".npmrc" or ".npmrc.local") and use a
proper negation entry "!.npmrc" (no leading space) to keep the safe project
.npmrc; update the entries referenced in the diff so the ignore rule and the
negation are valid gitignore patterns.

In `@packages/tempo/plan/licensing_architecture.md`:
- Line 28: The global discovery key is inconsistent: some places use
globalThis[TEMPO_DISCOVERY].license while others use window.__TEMPO_DISCOVERY__;
pick and apply one canonical name everywhere (e.g., attach __TEMPO_DISCOVERY__
to the global object) and update references accordingly; replace uses of
globalThis[TEMPO_DISCOVERY] and window.__TEMPO_DISCOVERY__ with a single
canonical access pattern (for example globalThis.__TEMPO_DISCOVERY__ or
globalThis[TEMPO_DISCOVERY] where TEMPO_DISCOVERY is a single exported constant)
and ensure any property access (like .license) uses that unified symbol across
the codebase and docs.
- Line 47: Update the "Cross-Session Persistence" section to explicitly mark
persisting the license JWT to localStorage as opt-in and unsafe by default:
state the XSS token-theft risk, recommend memory or sessionStorage as the
default, require a documented opt-in flag (e.g.,
"enableCrossSessionPersistence") before using localStorage, and add a short
mitigation checklist (use HttpOnly cookies where possible, CSP, input
sanitization, short TTL, and rotate/revoke tokens) so the architecture does not
normalize insecure storage of the license JWT.

In `@packages/tempo/plan/licensing_strategy.md`:
- Around line 44-48: The licensing discovery order in licensing_strategy.md
(items referencing Tempo.init, process.env.TEMPO_LICENSE, .temporc /
tempo.config.json, and globalThis.TEMPO_LICENSE) diverges from the architecture
spec; reconcile and update the document to the authoritative cascade (matching
the architecture plan’s config file locations and precedence), and ensure the
documented precedence aligns with the implementation points (e.g., Tempo.init(),
environment variable lookup, config-file lookup, and globalThis variable) so
core and plugins use the exact same source order.
- Line 1: The opening sentence "Now that we have the 'Plugin' architecture, we
need to determine how this will merge into a Tempo v2 strategy.." has an extra
period and slightly awkward phrasing; edit that first line to end with a single
period and consider clearer wording such as "Now that we have the 'Plugin'
architecture, we need to determine how this will merge into the Tempo v2
strategy" or "how this will merge with the Tempo v2 strategy" to improve
readability.
- Line 61: Update the Markdown heading "###Scenario 1" to include a space after
the hash marks so it reads "### Scenario 1" to comply with ATX heading syntax
and markdown linting; locate and replace the token "###Scenario 1" in the
licensing_strategy.md content (the heading at the start of the Scenario 1
section) with "### Scenario 1".

In `@packages/tempo/plan/v3-architecture-discoverability.md`:
- Line 15: The doc currently contradicts itself by saying the Registry should
publish `@magmacomputing/library` to public npmjs while later referring to
“private registry authentication”; update the document to pick one registry
model and make CI auth steps explicit: either (A) state that
`@magmacomputing/library` is published to public npmjs and remove or replace any
“private registry authentication” text with npmjs publish instructions and GH
Actions/NPM_TOKEN setup, or (B) state that packages are hosted on a private
registry and replace the “npmjs” mention with the private registry name and add
CI auth instructions (npmrc, registry URL, credentials/secrets) for the release
pipeline; ensure you edit the “Registry” line and the line containing “private
registry authentication” so both reflect the chosen model consistently.

In `@packages/tempo/src/plugin/term/term.type.ts`:
- Around line 31-33: The PremiumPlugin interface currently types status as
string; update it to the LICENSE enum type used by
Internal.LicenseState/Internal.ValidationResult (i.e. replace status?: string
with status?: enums.LICENSE or the LICENSE type alias in scope) so merging
license metadata into PremiumPlugin is type-safe; ensure any necessary
import/namespace (enums or LICENSE) is added and update usages of PremiumPlugin
that set status to use the enum values.

In `@packages/tempo/src/support/support.init.ts`:
- Around line 148-149: The current guard (runtime.license.key !== key) prevents
re-verification when the same license token is re-applied; remove or change that
guard so decodeJWT(key) and the verification flow always run when extend({
license }) is called (or add an explicit force flag to trigger re-check). Update
the conditional around runtime.license.key and decodeJWT(key) in support.init
(and the identical check in the extend({ license }) path) to allow re-running
the JWT decode/verification even if the incoming key equals runtime.license.key,
then continue the existing verification/update logic as before.
- Around line 166-179: validator.verify() is unhandled for rejections and its
async result can overwrite newer license state; capture a snapshot of the
current license identity before calling validator.verify() (e.g., const
initialJti = runtime.license.jti) and in the Promise resolution only apply res
to runtime.license if runtime.license.jti === initialJti (ignore stale results),
and add a .catch handler to handle Promise rejection by setting
runtime.license.error/status appropriately and calling logWarn; reference
validator.verify(), runtime.license, res.jti and LICENSE to locate where to
apply these changes.

In `@packages/tempo/test/plugins/licensing.full.test.ts`:
- Line 139: Replace the fragile "await new Promise(r => setTimeout(r, 0))"
polling with deterministic waiting: either use Vitest's vi.waitFor(() => /*
condition */) to poll the expected state change (e.g., vi.waitFor(() =>
someSpyOrStateHasBeenCalled())) or, if the module under test exposes the
init/reckoning promise, await that promise directly (e.g., await initReckoning()
or await returnedPromise) instead of setTimeout; update both occurrences of "new
Promise(r => setTimeout(r, 0))" so tests wait deterministically.
- Line 27: Replace the Node-only Buffer usage with a portable base64 helper: add
a small function base64Encode(input) that returns
Buffer.from(input).toString('base64') when Buffer exists, otherwise uses
TextEncoder + btoa (e.g. btoa(String.fromCharCode(...new
TextEncoder().encode(input)))) to produce base64 in browser/Deno; then replace
the Buffer.from(JSON.stringify(payload)).toString('base64') call used to build
mockToken (and the five other occurrences) with
base64Encode(JSON.stringify(payload)).
- Around line 115-127: Move the vi.mock('#tempo/license', ...) call out of the
test function into module scope so Vitest can hoist it; replace the inline class
behavior with a default mock and then in individual tests override behavior
using vi.mocked(...) or Validator.mockImplementation(...) to return the desired
verify() shape (referencing the Validator symbol and the existing verify method)
so each test can customize status/scopes without relying on an in-test vi.mock
call.
- Around line 152-156: The Revoked-status test relies on a mock created inside
another test; move the module mock for '#tempo/license' out of the test body to
module scope so Vitest hoists it (replace the in-test vi.mock(...) with a
top-level vi.mock('#tempo/license', ...)), then update the per-test behavior by
calling vi.mocked((await
import('#tempo/license'))).Validator.prototype.verify.mockImplementation(...) or
use vi.spyOn((await import('#tempo/license')),
'Validator').prototype.verify.mockImplementation(...) inside each test to set
the specific return for that scenario; this ensures vi.clearAllMocks() in
beforeEach won't remove the base mock and each test (e.g., the "transitions from
Pending to Active" and "handles Revoked status" tests) sets its own verify
behavior via Validator.prototype.verify.

In `@packages/tempo/vitest.config.ts`:
- Line 52: The vitest alias entries mapping /^#tempo\/license$/ ->
resolve(__dirname, '../../../tempo-plugin/packages/_core/dist/index.js') (and
the duplicate mapping later) hardcode an external repo path that doesn't exist;
remove those alias objects from the aliases array in vitest.config.ts so
Node/npm can resolve "#tempo/license" via the package
"@magmacomputing/tempo-plugin-_core" declared in package.json, or alternatively
replace the alias replacement with the package name
"@magmacomputing/tempo-plugin-_core" instead of the file-system resolve call to
let standard npm resolution handle it (update the alias objects referenced by
the find /^#tempo\/license$/ pattern and any duplicate entries).

---

Outside diff comments:
In `@packages/tempo/src/tempo.class.ts`:
- Around line 1338-1354: Tempo.#terms are being eagerly exposed by the define
closure and this.#setLazy without enforcing the premium-term/license check used
elsewhere; update the define installation so it first checks the same
premium-term guard used in the delegator callback path before invoking
term.resolve or term.define, returning a license-denied placeholder or throwing
as the delegator does when status is Revoked/Expired/Invalid; ensure the check
is applied for both the primary key (where this.#setLazy is called with true)
and the optional term.scope (false), and keep references to term.resolve,
term.define and this.toDateTime() intact so only the pre-check is added rather
than changing resolution logic.

---

Nitpick comments:
In `@packages/tempo/rollup.config.js`:
- Line 11: The rollup config's licensePath constant (licensePath in
packages/tempo/rollup.config.js) points to
../../../tempo-plugin/packages/_core/dist/index.js which introduces a
build-order dependency; ensure the tempo-plugin/_core package is built before
packaging Tempo by updating the build pipeline (CI task order or npm/yarn
workspace scripts) to run the _core build prior to the Tempo bundle step, or
change the reference to a source-level import pattern that doesn't require the
compiled dist (e.g., point to the package entry or use the workspace package
name) so the module resolution succeeds when running rollup.

In `@packages/tempo/test/plugins/licensing.full.test.ts`:
- Line 10: The test currently bypasses TypeScript safety by using delete (rt as
any).state; instead add a proper test-reset helper in the runtime module (e.g.,
implement and export resetRuntimeForTesting() in support.runtime.ts) that
obtains the runtime via getRuntime() and performs the controlled state
deletion/reset internally, then update the test in licensing.full.test.ts to
call resetRuntimeForTesting() rather than deleting (rt as any).state; this keeps
the delete encapsulated, preserves type safety at call sites, and exposes a
clear API for test resets (reference symbols: getRuntime,
resetRuntimeForTesting, and the place where the test currently deletes
rt.state).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0865a451-25a1-4ebc-8fb6-4e551f8a3542

📥 Commits

Reviewing files that changed from the base of the PR and between f89d2b6 and 29694b1.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (36)
  • .gitignore
  • .npmrc
  • CHANGELOG.md
  • package.json
  • packages/library/LICENSE
  • packages/library/package.json
  • packages/library/src/common/storage.library.ts
  • packages/library/src/common/utility.library.ts
  • packages/library/src/tsconfig.json
  • packages/library/test/tsconfig.json
  • packages/library/tsconfig.json
  • packages/tempo/CHANGELOG.md
  • packages/tempo/package.json
  • packages/tempo/plan/licensing_architecture.md
  • packages/tempo/plan/licensing_strategy.md
  • packages/tempo/plan/v3-architecture-discoverability.md
  • packages/tempo/rollup.config.js
  • packages/tempo/src/library.index.ts
  • packages/tempo/src/module/module.parse.ts
  • packages/tempo/src/plugin/plugin.util.ts
  • packages/tempo/src/plugin/term/term.index.ts
  • packages/tempo/src/plugin/term/term.type.ts
  • packages/tempo/src/plugin/term/term.util.ts
  • packages/tempo/src/support/support.default.ts
  • packages/tempo/src/support/support.enum.ts
  • packages/tempo/src/support/support.index.ts
  • packages/tempo/src/support/support.init.ts
  • packages/tempo/src/support/support.runtime.ts
  • packages/tempo/src/support/support.symbol.ts
  • packages/tempo/src/tempo.class.ts
  • packages/tempo/src/tempo.type.ts
  • packages/tempo/src/tsconfig.json
  • packages/tempo/test/plugins/licensing.full.test.ts
  • packages/tempo/test/tsconfig.json
  • packages/tempo/vitest.config.ts
  • tsconfig.base.json
💤 Files with no reviewable changes (2)
  • packages/library/tsconfig.json
  • packages/library/test/tsconfig.json

Comment thread .gitignore Outdated
Comment thread packages/tempo/plan/licensing_architecture.md Outdated
Comment thread packages/tempo/plan/licensing_architecture.md Outdated
Comment thread packages/tempo/plan/licensing_strategy.md Outdated
Comment thread packages/tempo/plan/licensing_strategy.md Outdated
Comment thread packages/tempo/test/plugins/licensing.full.test.ts Outdated
Comment thread packages/tempo/test/plugins/licensing.full.test.ts Outdated
Comment thread packages/tempo/test/plugins/licensing.full.test.ts Outdated
Comment thread packages/tempo/test/plugins/licensing.full.test.ts Outdated
Comment thread packages/tempo/vitest.config.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
packages/tempo/src/support/support.init.ts (1)

160-167: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Strengthen stale-result guard for licenses without jti.

The current guard can pass stale results when tokens omit jti (both sides remain undefined). Capture activeKey (or a monotonic version) and require both identity checks before mutating runtime.license.

Also applies to: 180-182

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tempo/src/support/support.init.ts` around lines 160 - 167, The
stale-result guard using only initialJti can fail when jti is undefined; capture
an additional identity (e.g., current runtime.license.key or a monotonic counter
stored as activeKey) before calling Validator.verify inside Pledge.onResolve,
and require BOTH runtime.license.jti === initialJti AND runtime.license.key ===
activeKey (or the monotonic counter unchanged) before mutating runtime.license;
apply the same dual-check pattern to the other Pledge.onResolve instance(s) that
perform similar license mutation (the blocks that create new
Pledge<Internal.LicensingModule> and call Validator.verify).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/library/src/common/utility.library.ts`:
- Line 13: The JWT payload decoding uses atob or Buffer.from on the raw
base64url segment (variable part when computing payload), which will fail for
'-'/'_' and missing padding; normalize the base64url string before decoding by
replacing '-' with '+' and '_' with '/', then pad the string with '=' to a
length multiple of 4, and then decode (keeping the existing branch that uses
atob or Buffer.from). Update the payload construction logic (the line that sets
payload from atob/Buffer.from) to perform this normalization on part first.
- Around line 85-89: The runtime detection currently checks Node first using
isDefined(global.process?.versions?.node) which misidentifies Deno (it may
expose process.versions.node); change the order to detect Deno first by testing
isDefined(global.Deno) and returning { global, type: CONTEXT.Deno } before the
NodeJS check, keeping the isDefined helper and the existing return shapes (and
leaving the subsequent NodeJS branch that returns { global, type: CONTEXT.NodeJS
} unchanged).

In `@packages/tempo/package.json`:
- Line 116: The package mapping for "`#tempo/license`" points to
`@magmacomputing/tempo-plugin-_core` only in test overrides, but runtime code in
tempo.class.ts (dynamic import at/around line 789) requires the real package at
runtime; add "`@magmacomputing/tempo-plugin-_core`" to package.json "dependencies"
so consumers can resolve it, and keep the test stub mapping
(test/support/license-stub-pkg) only for tests (e.g., in devDependencies or test
override), ensuring the production mapping still resolves to the real package.

In `@packages/tempo/rollup.config.js`:
- Around line 113-129: The entryFileNames routing currently returns only the
basename (name) which causes collisions when preserveModules: true; update the
entryFileNames logic (the function using id, name and rel/path.relative) to
include path context or a short hash so outputs are unique: instead of returning
`lib/${name}.js` or `lic/${name}.js` use the relative path segments (e.g.,
derive a modulePath from rel or path.dirname(rel) and emit
`lib/${modulePath}/${name}.js` or `lic/${modulePath}/${name}.js`), or append a
deterministic short hash of id when path context is not applicable; keep the
same id/name/rel variables and ensure node path separators are normalized for
output.

In `@packages/tempo/src/support/support.runtime.ts`:
- Around line 172-185: resetRuntimeForTesting() currently clears license fields
but doesn't prevent pending async verify() callbacks from repopulating runtime.
Modify resetRuntimeForTesting() (in support.runtime.ts) to atomically bump a
verification token/version on the runtime (e.g., set rt.license.jti to a fresh
unique value or increment rt._verifyVersion) so any in-flight verify completions
can be detected as stale; then update the verify completion path (the
function/method that applies verification results to getRuntime()/rt.license) to
check that the token/version from when the verification started matches the
current runtime token/version and refuse to apply results if they differ. Ensure
the token is set before clearing other license fields and is used consistently
by verify() and its callbacks.

---

Duplicate comments:
In `@packages/tempo/src/support/support.init.ts`:
- Around line 160-167: The stale-result guard using only initialJti can fail
when jti is undefined; capture an additional identity (e.g., current
runtime.license.key or a monotonic counter stored as activeKey) before calling
Validator.verify inside Pledge.onResolve, and require BOTH runtime.license.jti
=== initialJti AND runtime.license.key === activeKey (or the monotonic counter
unchanged) before mutating runtime.license; apply the same dual-check pattern to
the other Pledge.onResolve instance(s) that perform similar license mutation
(the blocks that create new Pledge<Internal.LicensingModule> and call
Validator.verify).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 87d582cd-9a6c-4565-b010-150bddf40496

📥 Commits

Reviewing files that changed from the base of the PR and between 29694b1 and b9c760e.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (19)
  • .gitignore
  • packages/library/src/common/utility.library.ts
  • packages/tempo/CHANGELOG.md
  • packages/tempo/doc/releases/v2.x.md
  • packages/tempo/package.json
  • packages/tempo/plan/licensing_architecture.md
  • packages/tempo/plan/licensing_strategy.md
  • packages/tempo/plan/v3-architecture-discoverability.md
  • packages/tempo/rollup.config.js
  • packages/tempo/src/plugin/term/term.type.ts
  • packages/tempo/src/support/support.init.ts
  • packages/tempo/src/support/support.runtime.ts
  • packages/tempo/src/tempo.class.ts
  • packages/tempo/src/tsconfig.json
  • packages/tempo/test/plugins/licensing.full.test.ts
  • packages/tempo/test/support/license-stub-pkg/index.ts
  • packages/tempo/test/support/license-stub-pkg/package.json
  • packages/tempo/test/tsconfig.json
  • packages/tempo/vitest.config.ts
✅ Files skipped from review due to trivial changes (3)
  • packages/tempo/test/support/license-stub-pkg/package.json
  • packages/tempo/doc/releases/v2.x.md
  • packages/tempo/plan/v3-architecture-discoverability.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • .gitignore
  • packages/tempo/test/tsconfig.json
  • packages/tempo/src/tsconfig.json
  • packages/tempo/plan/licensing_architecture.md
  • packages/tempo/test/plugins/licensing.full.test.ts
  • packages/tempo/src/tempo.class.ts

Comment thread packages/library/src/common/utility.library.ts Outdated
Comment thread packages/library/src/common/utility.library.ts Outdated
Comment thread packages/tempo/package.json Outdated
Comment thread packages/tempo/rollup.config.js Outdated
Comment thread packages/tempo/src/support/support.runtime.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/tempo/src/support/support.license.ts (1)

9-14: ⚡ Quick win

Replace Promise<any> with a concrete result type.

Line 9 weakens type guarantees on a core licensing contract. A typed return keeps call-sites safe and consistent with the new type-focused architecture.

Proposed fix
+type CommunityLicenseVerification = {
+  status: 'active';
+  scopes: { community: true };
+  error: null;
+};
+
 export class Validator {
 	constructor(public key: string) { }
-	async verify(): Promise<any> {
+	async verify(): Promise<CommunityLicenseVerification> {
 		return {
 			status: 'active', // Default to active for community use
 			scopes: { community: true },
 			error: null
 		};
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tempo/src/support/support.license.ts` around lines 9 - 14, The
verify method currently returns Promise<any>; define a concrete result type
(e.g., LicenseVerificationResult or LicenseStatus) that models the returned
object shape (status: string, scopes: { community: boolean }, error: string |
null) and update the method signature from verify(): Promise<any> to verify():
Promise<LicenseVerificationResult>; implement or export that interface/type in
the same module (or a shared types file) and update any call-sites or imports to
use the new type.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/tempo/bin/resolve-types.ts`:
- Around line 16-17: The code currently hardcodes LIC_SRC_DIR and writes to
LIC_DEST_DIR causing broken downstream types when
'../../../tempo-plugin/packages/@core/dist' is missing; update resolve-types.ts
to first check for existence of LIC_SRC_DIR (using fs.existsSync or fs.stat)
and: if it exists, proceed with the current copy/rewrite logic that targets
LIC_DEST_DIR; if it does not, skip the copy/rewrite and either create
LIC_DEST_DIR with a small fallback index.d.ts that re-exports or declares
minimal types or emit a clear console.warn + exit code depending on desired
behavior; ensure references to LIC_SRC_DIR, LIC_DEST_DIR and DIST_DIR are used
consistently and do not assume presence of external repo.

In `@packages/tempo/package.json`:
- Line 119: The package.json's import map entry "`#tempo/license`" points to a
source-only path "./src/support/support.license.ts" which won't be published;
change the import map to use a conditional mapping that prefers built artifacts
and falls back to source only for dev – e.g., update the "`#tempo/license`" entry
to reference the built file under dist (./dist/support/support.license.js or .ts
as appropriate) as the default and add a development-only conditional mapping to
./src/support/support.license.ts so consumers resolve to the published dist file
while local dev still works; locate the "`#tempo/license`" key in package.json and
mirror this approach for any other src-based mappings to match the package
"files" field.

---

Nitpick comments:
In `@packages/tempo/src/support/support.license.ts`:
- Around line 9-14: The verify method currently returns Promise<any>; define a
concrete result type (e.g., LicenseVerificationResult or LicenseStatus) that
models the returned object shape (status: string, scopes: { community: boolean
}, error: string | null) and update the method signature from verify():
Promise<any> to verify(): Promise<LicenseVerificationResult>; implement or
export that interface/type in the same module (or a shared types file) and
update any call-sites or imports to use the new type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2c3329bb-6eac-4037-a4d2-a4a88a01d625

📥 Commits

Reviewing files that changed from the base of the PR and between b9c760e and 6b753e5.

📒 Files selected for processing (9)
  • packages/library/src/common/utility.library.ts
  • packages/tempo/CHANGELOG.md
  • packages/tempo/bin/resolve-types.ts
  • packages/tempo/package.json
  • packages/tempo/rollup.config.js
  • packages/tempo/src/support/support.init.ts
  • packages/tempo/src/support/support.license.ts
  • packages/tempo/src/support/support.runtime.ts
  • packages/tempo/vitest.config.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/tempo/CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/tempo/src/support/support.runtime.ts
  • packages/library/src/common/utility.library.ts
  • packages/tempo/src/support/support.init.ts

Comment on lines +16 to +17
const LIC_SRC_DIR = path.resolve('../../../tempo-plugin/packages/@core/dist');
const LIC_DEST_DIR = path.resolve(DIST_DIR, 'lic');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

License type generation is coupled to a hardcoded external repo path.

The new #tempo/license d.ts rewrite depends on LIC_SRC_DIR existing at ../../../tempo-plugin/packages/@core/dist. If that path is missing, dist/lic types may not be produced while declarations still rewrite to lic/index.js, causing broken type resolution downstream.

Proposed fix
-const LIC_SRC_DIR = path.resolve('../../../tempo-plugin/packages/@core/dist');
 const LIC_DEST_DIR = path.resolve(DIST_DIR, 'lic');
+const COMMUNITY_LIC_DTS = path.resolve(DIST_DIR, 'support/support.license.d.ts');
+const PREMIUM_LICENSE_PATH = process.env.TEMPO_LICENSE_PATH
+  ? path.resolve(process.env.TEMPO_LICENSE_PATH)
+  : null;
+const LIC_SRC_DIR = PREMIUM_LICENSE_PATH ? path.dirname(PREMIUM_LICENSE_PATH) : null;

@@
-if (fs.existsSync(LIC_SRC_DIR)) {
+if (LIC_SRC_DIR && fs.existsSync(LIC_SRC_DIR)) {
   if (!fs.existsSync(LIC_DEST_DIR)) fs.mkdirSync(LIC_DEST_DIR, { recursive: true });
   const licFiles = fs.readdirSync(LIC_SRC_DIR).filter(f => f.endsWith('.d.ts'));
   licFiles.forEach(file => {
     fs.copyFileSync(path.join(LIC_SRC_DIR, file), path.join(LIC_DEST_DIR, file));
   });
 }
+
+// Always guarantee a lic/index.d.ts fallback for community builds
+if (!fs.existsSync(LIC_DEST_DIR)) fs.mkdirSync(LIC_DEST_DIR, { recursive: true });
+if (!fs.existsSync(path.join(LIC_DEST_DIR, 'index.d.ts')) && fs.existsSync(COMMUNITY_LIC_DTS)) {
+  fs.copyFileSync(COMMUNITY_LIC_DTS, path.join(LIC_DEST_DIR, 'index.d.ts'));
+}

Also applies to: 41-47, 79-99

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tempo/bin/resolve-types.ts` around lines 16 - 17, The code currently
hardcodes LIC_SRC_DIR and writes to LIC_DEST_DIR causing broken downstream types
when '../../../tempo-plugin/packages/@core/dist' is missing; update
resolve-types.ts to first check for existence of LIC_SRC_DIR (using
fs.existsSync or fs.stat) and: if it exists, proceed with the current
copy/rewrite logic that targets LIC_DEST_DIR; if it does not, skip the
copy/rewrite and either create LIC_DEST_DIR with a small fallback index.d.ts
that re-exports or declares minimal types or emit a clear console.warn + exit
code depending on desired behavior; ensure references to LIC_SRC_DIR,
LIC_DEST_DIR and DIST_DIR are used consistently and do not assume presence of
external repo.

"development": "./src/module/module.index.ts",
"default": "./dist/module/module.index.js"
},
"#tempo/license": "./src/support/support.license.ts",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

#tempo/license points to a non-shipped source path.

Line 119 maps #tempo/license to ./src/support/support.license.ts, but src/ is not published (see files), so consumer resolution can break after publish. Use a conditional import map with a dist default.

Proposed fix
-    "`#tempo/license`": "./src/support/support.license.ts",
+    "`#tempo/license`": {
+      "development": "./src/support/support.license.ts",
+      "default": "./dist/lic/index.js"
+    },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tempo/package.json` at line 119, The package.json's import map entry
"`#tempo/license`" points to a source-only path "./src/support/support.license.ts"
which won't be published; change the import map to use a conditional mapping
that prefers built artifacts and falls back to source only for dev – e.g.,
update the "`#tempo/license`" entry to reference the built file under dist
(./dist/support/support.license.js or .ts as appropriate) as the default and add
a development-only conditional mapping to ./src/support/support.license.ts so
consumers resolve to the published dist file while local dev still works; locate
the "`#tempo/license`" key in package.json and mirror this approach for any other
src-based mappings to match the package "files" field.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant