Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,9 @@ Thumbs.db
**/.vitepress/cache/
**/.vitepress/dist/
**/doc/api/

# Secrets and credentials
.env
.env.*
.npmrc (global/local if they contain tokens)
! .npmrc (the one we created is safe as it uses variables)
Comment thread
magmacomputing marked this conversation as resolved.
Outdated
3 changes: 3 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Authentication for npmjs
# Set this in your environment as NPM_TOKEN for CI/CD
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
19 changes: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,24 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [2.10.0] - 2026-05-11

### Added
- **Licensing Architecture Preparation**: Initial work to support licensed plugins and features.

### Changed
- **Term Collision Enforcement**: Term plugin registration now throws a fatal error on naming collisions (key/scope) to prevent silent configuration failures.

## [2.9.3] - 2026-05-11

### Added
- **Fractional Resolution**: Numeric inputs now support fractional components with nanosecond precision using BigInt math.
- **Hardened AliasContext**: Improved chainable context for functional aliases with full API parity.
- **Epoch Support**: Enhanced detection for 9-10 digit Epoch timestamps.

### Fixed
- **Mutation Safety**: Eliminated side-effects on global configuration during parsing.
- **Normalizer Memory**: Fixed state leakage in alias resolution.

## [2.8.0] - 2026-04-30

Expand Down
774 changes: 140 additions & 634 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tempo-monorepo",
"version": "2.9.3",
"version": "2.10.0",
"private": true,
"description": "Magma Computing Monorepo",
"repository": {
Expand All @@ -26,16 +26,16 @@
"devDependencies": {
"@js-temporal/polyfill": "^0.5.1",
"@rollup/plugin-node-resolve": "^16.0.3",
"@types/google.maps": "^3.64.0",
"@types/google.maps": "^3.64.1",
"@types/hammerjs": "^2.0.46",
"@types/jquery": "^4.0.0",
"@types/node": "^25.6.2",
"@types/node": "^25.8.0",
"@vitest/ui": "^2.1.9",
"cross-env": "^10.1.0",
"markdown-it-mathjax3": "^4.3.2",
"rollup": "^4.60.3",
"rollup": "^4.60.4",
"tslib": "^2.8.1",
"tsx": "^4.21.0",
"tsx": "^4.22.0",
"typescript": "^6.0.3",
"vitest": "^2.1.9"
},
Expand Down
21 changes: 21 additions & 0 deletions packages/library/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Magma Computing

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
5 changes: 4 additions & 1 deletion packages/library/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@magmacomputing/library",
"version": "2.9.3",
"version": "2.10.0",
"description": "Shared utility library for Tempo",
"author": "Magma Computing Solutions",
"license": "MIT",
Expand All @@ -9,6 +9,9 @@
"url": "git+https://github.com/magmacomputing/magma.git",
"directory": "packages/library"
},
"publishConfig": {
"access": "public"
},
"homepage": "https://github.com/magmacomputing/magma/tree/main/packages/library#readme",
"bugs": {
"url": "https://github.com/magmacomputing/magma/issues"
Expand Down
7 changes: 4 additions & 3 deletions packages/library/src/common/storage.library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ export function getStorage<T>(key?: string, dflt?: T): T | undefined {

case CONTEXT.NodeJS:
store = context.global.process.env[key];
if (key === '$Tempo' && !store) {
// skip debug log for production/test clean-up
}
break;

case CONTEXT.Deno:
store = context.global.Deno.env.get(key);
break;

case CONTEXT.GoogleAppsScript:
Expand Down
15 changes: 14 additions & 1 deletion packages/library/src/common/utility.library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ import type { Secure, ValueOf } from '#library/type.library.js';

/** General utility functions */

/** fast, unverified decode of a JWT payload */
export const decodeJWT = <T = any>(jwt: string): T | null => {
try {
const part = jwt.split('.')[1];
if (!part) return null;
const payload = typeof atob === 'function' ? atob(part) : Buffer.from(part, 'base64').toString();
Comment thread
magmacomputing marked this conversation as resolved.
Outdated
return JSON.parse(payload);
} catch { return null; }
}

/** analyze the Call Stack to determine calling Function's name */
export const getCaller = () => {
const stackTrace = new Error().stack // only tested in latest FF and Chrome
Expand Down Expand Up @@ -61,6 +71,9 @@ export const getContext = (): Context => {
if (isDefined(global.process?.versions?.node))
return { global, type: CONTEXT.NodeJS };

if (isDefined(global.Deno))
return { global, type: CONTEXT.Deno };
Comment thread
magmacomputing marked this conversation as resolved.
Outdated

return { global, type: CONTEXT.Unknown };
}

Expand All @@ -85,7 +98,7 @@ export function deepFreeze<const T extends object>(obj: T, options?: { skip?: We
export function deepFreeze<const T extends object>(obj: T, options?: { skip?: WeakSet<object> } | WeakSet<object>, seen: WeakSet<object> = new WeakSet<object>()): Secure<T> {
// Support both old and new signatures for backward compatibility
const skip = (options instanceof WeakSet) ? options : (options?.skip ?? EMPTY_SKIP);

if (isPrimitive(obj) || Object.isFrozen(obj) || seen.has(obj) || skip.has(obj))
return obj as Secure<T>;

Expand Down
1 change: 1 addition & 0 deletions packages/library/src/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"composite": true,
"lib": [
"ESNext",
"ESNext.Temporal",
"DOM"
],
"types": [
Expand Down
6 changes: 0 additions & 6 deletions packages/library/test/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,6 @@
],
"#server/*": [
"../src/server/*"
],
"#tempo": [
"../../tempo/src/tempo.index.ts"
],
"#tempo/*": [
"../../tempo/src/*"
]
}
},
Expand Down
6 changes: 0 additions & 6 deletions packages/library/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,6 @@
],
"#server/*": [
"./src/server/*"
],
"#tempo": [
"../tempo/src/tempo.index.ts"
],
"#tempo/*": [
"../tempo/src/*"
]
}
},
Expand Down
8 changes: 8 additions & 0 deletions packages/tempo/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.10.0] - 2026-05-11

### Added
- **Licensing Architecture Preparation**: Initial work to support licensed plugins and features.

### Changed
- **Term Collision Enforcement**: Term plugin registration now throws a fatal error on naming collisions (key/scope) to prevent silent configuration failures.

## [2.9.3] - 2026-05-11

### Added
Expand Down
6 changes: 4 additions & 2 deletions packages/tempo/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@magmacomputing/tempo",
"version": "2.9.3",
"version": "2.10.0",
"description": "The Tempo core library",
"author": "Magma Computing Solutions",
"license": "MIT",
Expand Down Expand Up @@ -113,6 +113,7 @@
"development": "./src/module/module.index.ts",
"default": "./dist/module/module.index.js"
},
"#tempo/license": "@magmacomputing/tempo-plugin-_core",
Comment thread
magmacomputing marked this conversation as resolved.
Outdated
"#tempo/*.js": {
"development": "./src/*.ts",
"default": "./dist/*.js"
Expand Down Expand Up @@ -219,7 +220,8 @@
},
"devDependencies": {
"@js-temporal/polyfill": "^0.5.1",
"@magmacomputing/library": "2.9.3",
"@magmacomputing/library": "2.10.0",
"@magmacomputing/tempo-plugin-_core": "file:../../../tempo-plugin/packages/_core",
"@rollup/plugin-alias": "^6.0.0",
"magic-string": "^0.30.21",
"typedoc": "^0.28.19",
Expand Down
88 changes: 57 additions & 31 deletions packages/tempo/plan/licensing_architecture.md
Original file line number Diff line number Diff line change
@@ -1,48 +1,74 @@
# Tempo Licensing Architecture (v2.9.4 Proposal)
# Tempo Licensing Architecture & Plugin Strategy

## Objective
Enable a robust, secure, and flexible licensing mechanism for premium Tempo plugins without bloating the core engine or compromising user security.
Implement a secure, user-friendly licensing system for Tempo plugins that enables monetization and IP protection without creating high friction for developers.

## Architectural Decisions
## The "No-PAT" Distribution Model
To eliminate developer friction (configuring registries/PATs), all plugins are distributed via the public npmjs registry.

### 1. Separation of Concerns
- **Core Tempo**: Acts only as a "Parking Spot" for the license string. It does NOT contain logic for decoding JWTs or verifying signatures.
- **Plugins**: House all enforcement logic. The plugin's `install()` method is responsible for fetching the license from the state and validating it.
- **Storage**: Private source code resides in Magma Computing's GitHub organization.
- **Distribution**: Plugins are built, minified, and obfuscated before being published as public packages under the `@magmacomputing` scope (e.g., `@magmacomputing/plugin-term`).
- **Access**: Anyone can `npm install`, but functionality is gated by the "Tempo Activation Key" (JWT).
- **Free Tier**: The "Astrological Sign" plugin serves as a free reference implementation for users to test the activation and support model.

### 2. Internal State Branching
To prevent sensitive license keys from appearing in diagnostic logs:
- **Location**: The license will reside in a separate branch of the internal state (e.g., `state.auth` or directly on `state.license`).
- **Isolation**: It must NOT be part of the `config` or `parse` objects, which are frequently passed to `Logify` for debugging.
## Activation & Validation

### 3. Cascade of License Discovery
The engine will look for a license key in the following order:
### 1. The Tempo Activation Key (JWT)
The key is a JSON Web Token issued to the customer. It supports three tiers of usage:

| Claim | Standard/Trial | Enterprise/Distribution |
| :--- | :--- | :--- |
| `aud` (Audience) | Array of domains (e.g. `["localhost", "site.com"]`) | `"*"` (Wildcard - works on any domain) |
| `exp` (Expiry) | Unix timestamp (e.g. 1 year from issue) | `0` (Perpetual - never expires) |
| `jti` | Unique ID for revocation | Unique ID for revocation |

### 2. Cascade of License Discovery
Plugins will look for the activation key in the following order:
1. **Explicit**: `Tempo.init({ license: '...' })`
2. **Discovery**: `globalThis[TEMPO_DISCOVERY].license`
Comment thread
magmacomputing marked this conversation as resolved.
Outdated
3. **Environment**: `process.env.TEMPO_LICENSE` (Server-side)
4. **Storage**: `localStorage.getItem('tempo_license')` (Client-side)

## Security & Risks
- **XSS**: Keys in `localStorage` are vulnerable; documentation must warn users to prefer secure server-side injection where possible.
- **Leakage**: Even with separate branching, we must ensure internal state dumps (for support) redact this branch by default.
### 3. Verification Logic
- **Domain Locking**: The plugin verifies `window.location.hostname` against the `aud` claim (unless `aud` is `*`).
- **Grace Period**: If a key is expired, the plugin enters a 7-day "Grace Period," allowing full functionality while emitting a `console.warn`.
- **Revocation**:
- Plugins fetch a **Signed Revocation List** (JWS) from Magma's servers every **7 days**.
- The fetch is shared across all Tempo plugins to minimize overhead.
- If offline, the plugin "Fails Open" and relies on the JWT's internal expiry.
- The list is self-cleaning; keys are removed once they naturally expire.

### 4. The License "Wallet" (Discovery & Persistence)
To prevent "Token Fatigue" and the need to re-supply keys, Tempo and its plugins treat the environment as a persistent wallet.

- **One Key, Many Scopes**: A single JWT can contain multiple plugin identifiers in its `scope` claim (e.g., `scope: ["term", "ticker"]`).
- **Global Discovery (Browser)**: Developers can define `window.__TEMPO_DISCOVERY__ = { license: '...' }` at the very top of their HTML. Any Tempo instance or plugin will automatically "hydrate" from this global wallet.
- **Internal Stashing**: Once a license is discovered or provided via `init()`, Tempo core caches it in its internal static state. This ensures that a plugin imported in a different module (or a secondary Tempo instance) can still find the active license without being explicitly passed a key.
- **Cross-Session Persistence**: If a valid license is detected, the plugin can optionally persist it to `localStorage`. This allows subsequent user sessions to remain "Activated" even if the developer removes the explicit key from a specific page.
Comment thread
magmacomputing marked this conversation as resolved.
Outdated

## Remote Invalidation & Revocation
### 5. Revocation Infrastructure (The Control Tower)
To maintain security without manual intervention on the customer side:

Since Tempo is designed for offline-first stability, we avoid a "mandatory phone-home" on every instance. Instead, we propose the following strategies for invalidating compromised licenses:
- **Host**: `api.magmacomputing.com.au`
- **Endpoint**: `/tempo/v1/revoked.jws` (Versioned for future-proofing).
- **Format**: The list is a Signed JWS. Plugins verify it using an embedded Public Key.
- **Management**:
- A local `registry/` folder in the private `tempo-plugin` repo tracks the revocation state.
- CLI tools (`npm run license:revoke`) handle the signing and deployment of the updated JWS to the production host.
- **Security**: The **Private Key** for signing is stored as a GH Secret and never committed. The **Public Key** is baked into the obfuscated plugin source.
- **Fail-Safe**: If the endpoint is unreachable, plugins "Fail Open" and rely on the internal JWT expiry to ensure no service disruption for legitimate users.

### 1. Short-lived JWTs (Rotation)
- Issue licenses with 30- or 90-day expiry.
- Use a lightweight refresh mechanism to update the license during application build or bootstrap.
- **Benefit**: Naturally limits the window of exposure for any single leaked key.
## Implementation Roadmap

### 2. Revocation Lists (Blacklisting)
- Plugins can periodically fetch a `revoked.json` list of blacklisted JWT IDs (`jti`).
- If a breach is detected, the `jti` is added to the list, and the plugin disables itself upon the next update/fetch.
### Phase 1: Support Infrastructure
- [ ] Add `license` to `Internal.State` and `BaseOptions` in Tempo Core.
- [ ] Implement the Discovery Cascade and Shared Revocation Service.
- [ ] Ensure `Logify` and internal state dumps redact the `state.license` branch.

### 3. Graceful Fallback
- If a license is invalid or expired, the plugin should not crash the application.
- **Behavior**: Downgrade to "Limited" mode, log a warning, but ensure the core Tempo engine continues to function.
### Phase 2: Reference Plugin (Astro)
- [ ] Create the `@magmacomputing/plugin-astro` workspace.
- [ ] Implement JWS signature verification and JWT decoding.
- [ ] Set up the obfuscated build pipeline for public npmjs distribution.

## Next Steps (v2.9.4)
- Add `license` to `Internal.State` and `BaseOptions`.
- Update `support.init.ts` to implement the Discovery Cascade.
- Provide a reference implementation in the `tempo-plugin` mono-repo showing how to decode a JWT using Tempo's native date utilities.
### Phase 3: Commercial Plugins
- [ ] Roll out `term`, `ticker`, and other premium extensions using the proven Astro model.
Loading
Loading