@nexus-mindgarden/plugin-license-foundation 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/client.d.ts +31 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +67 -0
- package/dist/client.js.map +1 -0
- package/dist/entitle.d.ts +23 -0
- package/dist/entitle.d.ts.map +1 -0
- package/dist/entitle.js +64 -0
- package/dist/entitle.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/jwks.d.ts +26 -0
- package/dist/jwks.d.ts.map +1 -0
- package/dist/jwks.js +68 -0
- package/dist/jwks.js.map +1 -0
- package/dist/types.d.ts +50 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +21 -0
- package/dist/types.js.map +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexander Pajor
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @nexus-mindgarden/plugin-license-foundation
|
|
2
|
+
|
|
3
|
+
NEXUS plugin-entitlement **LicenseGate** for the host's `PluginManager.activate` seam — offline-first, default-deny, last-known-good grace, plus the entitle-on-activation call.
|
|
4
|
+
|
|
5
|
+
## Model
|
|
6
|
+
|
|
7
|
+
NEXUS issues plugin entitlements as **signed claims on the agent-JWT** (`plugins: string[]`, `ent_ver: int`), verifiable **offline via JWKS**. The host already verifies this JWT for app-licensing; this package adds the per-plugin gate. Revocation = `ent_ver` bump + SSE → host reconcile. (Wire: chatbus `#plugin-licensing`, nexus #5374 / agent #5377.)
|
|
8
|
+
|
|
9
|
+
## Quick use
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import {
|
|
13
|
+
createLicenseClient,
|
|
14
|
+
verifyEntitlementJwt,
|
|
15
|
+
remoteJwks,
|
|
16
|
+
entitlePlugin,
|
|
17
|
+
} from '@nexus-mindgarden/plugin-license-foundation'
|
|
18
|
+
|
|
19
|
+
// Cache the JWKS getter once (don't re-fetch per verify).
|
|
20
|
+
const jwks = remoteJwks('https://nexus.example/api/licenses/jwks.json')
|
|
21
|
+
|
|
22
|
+
const { gate } = createLicenseClient({
|
|
23
|
+
// Yield the current entitlement — from the host's already-verified agent-JWT
|
|
24
|
+
// claims, or by verifying the raw JWT here:
|
|
25
|
+
resolveEntitlement: async () => {
|
|
26
|
+
const jwt = await host.currentAgentJwt() // host-supplied
|
|
27
|
+
if (!jwt) return null
|
|
28
|
+
return verifyEntitlementJwt(jwt, {
|
|
29
|
+
jwks,
|
|
30
|
+
issuer: 'nexus.digitaleprojekte.at',
|
|
31
|
+
audience: 'nexus',
|
|
32
|
+
})
|
|
33
|
+
},
|
|
34
|
+
graceMs: 7 * 24 * 60 * 60 * 1000, // last-known-good offline window (host policy)
|
|
35
|
+
// failOpen: true, // ONLY for dev / enforce_entitlements=false
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// Drops into PluginManager.activate's LicenseGate.check seam:
|
|
39
|
+
const result = await gate.check({ pluginId: 'apex-2d', userId })
|
|
40
|
+
// → { ok: true } | { ok: false, reason: 'not_entitled' | 'no_entitlement' | 'stale_entitlement' | … }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Entitle on activation (free → grant, paid → 402)
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const r = await entitlePlugin({
|
|
47
|
+
endpoint: 'https://nexus.example/api/licenses/entitlements/plugin',
|
|
48
|
+
slug: 'apex-2d',
|
|
49
|
+
activationToken, // device credential (Bearer)
|
|
50
|
+
})
|
|
51
|
+
// free → { ok: true }
|
|
52
|
+
// paid → { ok: false, reason: 'purchase_required' }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Security
|
|
56
|
+
|
|
57
|
+
- **Default-deny:** no resolvable entitlement and no usable cache → `{ ok: false }`. `failOpen` is opt-in (dev / issuance-before-enforcement).
|
|
58
|
+
- **Alg-pinned:** `verifyEntitlementJwt` accepts **only** `EdDSA` — never `HS*`/`none`. Any verify failure throws (no permissive path).
|
|
59
|
+
- **Offline grace** is bounded by the cached `exp` + `graceMs` (host policy); past it → `stale_entitlement`, not a silent allow. `verifyEntitlementJwt` **requires** `exp` (an exp-less token → `invalid_claims`), so a verified entitlement's grace is always anchored on a signed freshness boundary. (A custom `resolveEntitlement` that yields an `exp`-less object anchors grace on last-resolved-online instead.)
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Entitlement, LicenseGate } from './types.js';
|
|
2
|
+
export interface CreateLicenseClientOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Yields the current entitlement, or `null` when none is available (e.g.
|
|
5
|
+
* offline and no token yet). May be sync or async. On `null`/throw the gate
|
|
6
|
+
* falls back to last-known-good within the grace window.
|
|
7
|
+
*/
|
|
8
|
+
resolveEntitlement: () => Promise<Entitlement | null> | Entitlement | null;
|
|
9
|
+
/**
|
|
10
|
+
* Last-known-good offline grace (ms past the cached entitlement's `exp`). When
|
|
11
|
+
* `resolveEntitlement` can't produce a fresh entitlement, a cached one is
|
|
12
|
+
* trusted until `exp + graceMs`. Default `0` (no grace → only fresh claims pass).
|
|
13
|
+
* Host-policy — NEXUS sends `exp` (short TTL), not a grace window.
|
|
14
|
+
*/
|
|
15
|
+
graceMs?: number;
|
|
16
|
+
/**
|
|
17
|
+
* When true, a gate that cannot resolve an entitlement AND has no usable cache
|
|
18
|
+
* returns `{ ok: true }` instead of denying. Default `false` (DEFAULT-DENY).
|
|
19
|
+
* Use only for dev / `enforce_entitlements=false` (issuance-before-enforcement).
|
|
20
|
+
*/
|
|
21
|
+
failOpen?: boolean;
|
|
22
|
+
/** Clock (ms epoch). Default `Date.now`. Injectable for tests. */
|
|
23
|
+
now?: () => number;
|
|
24
|
+
}
|
|
25
|
+
export interface LicenseClient {
|
|
26
|
+
gate: LicenseGate;
|
|
27
|
+
/** The cached last-known-good entitlement (for diagnostics / ent_ver reconcile). */
|
|
28
|
+
lastKnownGood(): Entitlement | null;
|
|
29
|
+
}
|
|
30
|
+
export declare function createLicenseClient(opts: CreateLicenseClientOptions): LicenseClient;
|
|
31
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAqB,MAAM,YAAY,CAAA;AAE7E,MAAM,WAAW,0BAA0B;IACzC;;;;OAIG;IACH,kBAAkB,EAAE,MAAM,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,WAAW,GAAG,IAAI,CAAA;IAC1E;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,kEAAkE;IAClE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,WAAW,CAAA;IACjB,oFAAoF;IACpF,aAAa,IAAI,WAAW,GAAG,IAAI,CAAA;CACpC;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,0BAA0B,GAAG,aAAa,CA8DnF"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// createLicenseClient — produces a LicenseGate for the host's PluginManager.activate
|
|
2
|
+
// seam. Offline-first (consumes verified entitlement claims; no online call on the
|
|
3
|
+
// hot path), default-deny, with a host-policy last-known-good grace window.
|
|
4
|
+
//
|
|
5
|
+
// The host wires `resolveEntitlement` to yield the current entitlement — either
|
|
6
|
+
// from its already-verified NEXUS agent-JWT claims, or via verifyEntitlementJwt
|
|
7
|
+
// (this package) against JWKS. Revocation (ent_ver bump + SSE) is the host's
|
|
8
|
+
// reconcile job; this gate just answers "is this plugin entitled, right now?".
|
|
9
|
+
export function createLicenseClient(opts) {
|
|
10
|
+
const graceMs = opts.graceMs ?? 0;
|
|
11
|
+
const failOpen = opts.failOpen ?? false;
|
|
12
|
+
const now = opts.now ?? Date.now;
|
|
13
|
+
let cache = null;
|
|
14
|
+
/** End of the trust window for a cached entitlement (ms epoch). */
|
|
15
|
+
function trustUntil(c) {
|
|
16
|
+
const base = c.entitlement.exp !== undefined ? c.entitlement.exp * 1000 : c.cachedAt;
|
|
17
|
+
return base + graceMs;
|
|
18
|
+
}
|
|
19
|
+
function decide(entitlement, pluginId) {
|
|
20
|
+
return entitlement.plugins.includes(pluginId)
|
|
21
|
+
? { ok: true }
|
|
22
|
+
: { ok: false, reason: 'not_entitled', message: `plugin '${pluginId}' not in entitlement` };
|
|
23
|
+
}
|
|
24
|
+
const gate = {
|
|
25
|
+
async check(input) {
|
|
26
|
+
let fresh = null;
|
|
27
|
+
let threw = false;
|
|
28
|
+
try {
|
|
29
|
+
fresh = await opts.resolveEntitlement();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
threw = true;
|
|
33
|
+
}
|
|
34
|
+
// Online path: a freshly-resolved entitlement is authoritative + cached.
|
|
35
|
+
if (fresh) {
|
|
36
|
+
cache = { entitlement: fresh, cachedAt: now() };
|
|
37
|
+
return decide(fresh, input.pluginId);
|
|
38
|
+
}
|
|
39
|
+
// Offline path: fall back to last-known-good within the grace window.
|
|
40
|
+
if (cache) {
|
|
41
|
+
if (now() <= trustUntil(cache)) {
|
|
42
|
+
return decide(cache.entitlement, input.pluginId);
|
|
43
|
+
}
|
|
44
|
+
// cache exists but is past exp + grace
|
|
45
|
+
if (failOpen)
|
|
46
|
+
return { ok: true };
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
reason: 'stale_entitlement',
|
|
50
|
+
message: 'cached entitlement past exp + grace window',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// No fresh entitlement and no cache at all.
|
|
54
|
+
if (failOpen)
|
|
55
|
+
return { ok: true };
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
reason: threw ? 'entitlement_error' : 'no_entitlement',
|
|
59
|
+
message: threw
|
|
60
|
+
? 'resolveEntitlement threw and no cache available'
|
|
61
|
+
: 'no entitlement available',
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
return { gate, lastKnownGood: () => cache?.entitlement ?? null };
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,mFAAmF;AACnF,4EAA4E;AAC5E,EAAE;AACF,gFAAgF;AAChF,gFAAgF;AAChF,6EAA6E;AAC7E,+EAA+E;AAkC/E,MAAM,UAAU,mBAAmB,CAAC,IAAgC;IAClE,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,CAAC,CAAA;IACjC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,KAAK,CAAA;IACvC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAA;IAEhC,IAAI,KAAK,GAA0D,IAAI,CAAA;IAEvE,mEAAmE;IACnE,SAAS,UAAU,CAAC,CAAiD;QACnE,MAAM,IAAI,GAAG,CAAC,CAAC,WAAW,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;QACpF,OAAO,IAAI,GAAG,OAAO,CAAA;IACvB,CAAC;IAED,SAAS,MAAM,CAAC,WAAwB,EAAE,QAAgB;QACxD,OAAO,WAAW,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAC3C,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE;YACd,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,QAAQ,sBAAsB,EAAE,CAAA;IAC/F,CAAC;IAED,MAAM,IAAI,GAAgB;QACxB,KAAK,CAAC,KAAK,CAAC,KAAK;YACf,IAAI,KAAK,GAAuB,IAAI,CAAA;YACpC,IAAI,KAAK,GAAG,KAAK,CAAA;YACjB,IAAI,CAAC;gBACH,KAAK,GAAG,MAAM,IAAI,CAAC,kBAAkB,EAAE,CAAA;YACzC,CAAC;YAAC,MAAM,CAAC;gBACP,KAAK,GAAG,IAAI,CAAA;YACd,CAAC;YAED,yEAAyE;YACzE,IAAI,KAAK,EAAE,CAAC;gBACV,KAAK,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE,CAAA;gBAC/C,OAAO,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAA;YACtC,CAAC;YAED,sEAAsE;YACtE,IAAI,KAAK,EAAE,CAAC;gBACV,IAAI,GAAG,EAAE,IAAI,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;oBAC/B,OAAO,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAA;gBAClD,CAAC;gBACD,uCAAuC;gBACvC,IAAI,QAAQ;oBAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;gBACjC,OAAO;oBACL,EAAE,EAAE,KAAK;oBACT,MAAM,EAAE,mBAAmB;oBAC3B,OAAO,EAAE,4CAA4C;iBACtD,CAAA;YACH,CAAC;YAED,4CAA4C;YAC5C,IAAI,QAAQ;gBAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;YACjC,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,gBAAgB;gBACtD,OAAO,EAAE,KAAK;oBACZ,CAAC,CAAC,iDAAiD;oBACnD,CAAC,CAAC,0BAA0B;aAC/B,CAAA;QACH,CAAC;KACF,CAAA;IAED,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,WAAW,IAAI,IAAI,EAAE,CAAA;AAClE,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { LicenseGateResult } from './types.js';
|
|
2
|
+
export interface EntitlePluginOptions {
|
|
3
|
+
/** Full entitle endpoint, e.g. `https://nexus.example/api/licenses/entitlements/plugin`. */
|
|
4
|
+
endpoint: string;
|
|
5
|
+
/** Plugin slug to entitle (the NEXUS-canonical slug, e.g. `apex-2d`). */
|
|
6
|
+
slug: string;
|
|
7
|
+
/** Device activation-token (Bearer) — the same credential used for getEntitlements. */
|
|
8
|
+
activationToken: string;
|
|
9
|
+
/** Optional fetch override (tests / custom agent). Default global fetch. */
|
|
10
|
+
fetchImpl?: typeof fetch;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Entitle a plugin on activation. Returns:
|
|
14
|
+
* - `{ ok: true }` on grant (free auto-grant, 2xx).
|
|
15
|
+
* - `{ ok: false, reason: 'purchase_required' }` on 402 (paid, not purchased) —
|
|
16
|
+
* deterministically mapped from `detail.reason === 'purchase_required'`.
|
|
17
|
+
* - `{ ok: false, reason: 'entitlement_error', message }` on any other failure.
|
|
18
|
+
*
|
|
19
|
+
* Never throws — always returns a LicenseGateResult (the activation seam wants a
|
|
20
|
+
* decision, not an exception).
|
|
21
|
+
*/
|
|
22
|
+
export declare function entitlePlugin(opts: EntitlePluginOptions): Promise<LicenseGateResult>;
|
|
23
|
+
//# sourceMappingURL=entitle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entitle.d.ts","sourceRoot":"","sources":["../src/entitle.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAEnD,MAAM,WAAW,oBAAoB;IACnC,4FAA4F;IAC5F,QAAQ,EAAE,MAAM,CAAA;IAChB,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAA;IACZ,uFAAuF;IACvF,eAAe,EAAE,MAAM,CAAA;IACvB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CACzB;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA6C1F"}
|
package/dist/entitle.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Entitle-on-activation — the optional online step at plugin activation.
|
|
2
|
+
//
|
|
3
|
+
// `POST /api/licenses/entitlements/plugin { plugin_slug }` (Bearer=activation_token),
|
|
4
|
+
// idempotent. Free plugins → auto-grant (2xx). Paid plugins → 402 with
|
|
5
|
+
// `{ detail: { reason: 'purchase_required', … } }` (until shop-checkout exists).
|
|
6
|
+
// After a successful grant, the host refreshes entitlements (cache + SSE handle
|
|
7
|
+
// the rest) so the LicenseGate sees the new slug.
|
|
8
|
+
/**
|
|
9
|
+
* Entitle a plugin on activation. Returns:
|
|
10
|
+
* - `{ ok: true }` on grant (free auto-grant, 2xx).
|
|
11
|
+
* - `{ ok: false, reason: 'purchase_required' }` on 402 (paid, not purchased) —
|
|
12
|
+
* deterministically mapped from `detail.reason === 'purchase_required'`.
|
|
13
|
+
* - `{ ok: false, reason: 'entitlement_error', message }` on any other failure.
|
|
14
|
+
*
|
|
15
|
+
* Never throws — always returns a LicenseGateResult (the activation seam wants a
|
|
16
|
+
* decision, not an exception).
|
|
17
|
+
*/
|
|
18
|
+
export async function entitlePlugin(opts) {
|
|
19
|
+
const fetchFn = opts.fetchImpl ?? fetch;
|
|
20
|
+
let res;
|
|
21
|
+
try {
|
|
22
|
+
res = await fetchFn(opts.endpoint, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: {
|
|
25
|
+
Authorization: `Bearer ${opts.activationToken}`,
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
Accept: 'application/json',
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify({ plugin_slug: opts.slug }),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
reason: 'entitlement_error',
|
|
36
|
+
message: `entitle fetch failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (res.ok)
|
|
40
|
+
return { ok: true };
|
|
41
|
+
if (res.status === 402) {
|
|
42
|
+
// Deterministic shape (nexus): { detail: { reason: 'purchase_required', message } }
|
|
43
|
+
let reasonField;
|
|
44
|
+
let message;
|
|
45
|
+
try {
|
|
46
|
+
const body = (await res.json());
|
|
47
|
+
reasonField = body?.detail?.reason;
|
|
48
|
+
message = typeof body?.detail?.message === 'string' ? body.detail.message : undefined;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// ignore parse error — treat as purchase_required by status anyway
|
|
52
|
+
}
|
|
53
|
+
if (reasonField === 'purchase_required' || reasonField === undefined) {
|
|
54
|
+
return { ok: false, reason: 'purchase_required', ...(message ? { message } : {}) };
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
reason: 'entitlement_error',
|
|
59
|
+
message: `402 with reason '${String(reasonField)}'`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return { ok: false, reason: 'entitlement_error', message: `entitle returned HTTP ${res.status}` };
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=entitle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entitle.js","sourceRoot":"","sources":["../src/entitle.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,EAAE;AACF,sFAAsF;AACtF,uEAAuE;AACvE,iFAAiF;AACjF,gFAAgF;AAChF,kDAAkD;AAelD;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAA0B;IAC5D,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,IAAI,KAAK,CAAA;IACvC,IAAI,GAAa,CAAA;IACjB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE;YACjC,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,aAAa,EAAE,UAAU,IAAI,CAAC,eAAe,EAAE;gBAC/C,cAAc,EAAE,kBAAkB;gBAClC,MAAM,EAAE,kBAAkB;aAC3B;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;SACjD,CAAC,CAAA;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,mBAAmB;YAC3B,OAAO,EAAE,yBAAyB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE;SACrF,CAAA;IACH,CAAC;IAED,IAAI,GAAG,CAAC,EAAE;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;IAE/B,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QACvB,oFAAoF;QACpF,IAAI,WAAoB,CAAA;QACxB,IAAI,OAA2B,CAAA;QAC/B,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAyD,CAAA;YACvF,WAAW,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,CAAA;YAClC,OAAO,GAAG,OAAO,IAAI,EAAE,MAAM,EAAE,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAA;QACvF,CAAC;QAAC,MAAM,CAAC;YACP,mEAAmE;QACrE,CAAC;QACD,IAAI,WAAW,KAAK,mBAAmB,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YACrE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAA;QACpF,CAAC;QACD,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,mBAAmB;YAC3B,OAAO,EAAE,oBAAoB,MAAM,CAAC,WAAW,CAAC,GAAG;SACpD,CAAA;IACH,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,OAAO,EAAE,yBAAyB,GAAG,CAAC,MAAM,EAAE,EAAE,CAAA;AACnG,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createLicenseClient, type CreateLicenseClientOptions, type LicenseClient, } from './client.js';
|
|
2
|
+
export { verifyEntitlementJwt, remoteJwks, LicenseVerifyError, type VerifyEntitlementJwtOptions, } from './jwks.js';
|
|
3
|
+
export { entitlePlugin, type EntitlePluginOptions } from './entitle.js';
|
|
4
|
+
export { EntitlementSchema, type Entitlement, type LicenseGate, type LicenseGateResult, type LicenseDenyReason, } from './types.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,mBAAmB,EACnB,KAAK,0BAA0B,EAC/B,KAAK,aAAa,GACnB,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,oBAAoB,EACpB,UAAU,EACV,kBAAkB,EAClB,KAAK,2BAA2B,GACjC,MAAM,WAAW,CAAA;AAClB,OAAO,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,MAAM,cAAc,CAAA;AACvE,OAAO,EACL,iBAAiB,EACjB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,GACvB,MAAM,YAAY,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// @nexus-mindgarden/plugin-license-foundation — NEXUS entitlement LicenseGate.
|
|
2
|
+
//
|
|
3
|
+
// Drops into the host's PluginManager.activate seam (`LicenseGate.check`). The
|
|
4
|
+
// host already verifies the NEXUS agent-JWT for app-licensing; this package adds
|
|
5
|
+
// the per-plugin gate (offline, default-deny, last-known-good grace) + the
|
|
6
|
+
// entitle-on-activation call. Wire-spec: chatbus #plugin-licensing (nexus #5374,
|
|
7
|
+
// agent #5377). The `checkLicense` stub in HOST-INTEGRATION-GUIDE §2.3 becomes real.
|
|
8
|
+
export { createLicenseClient, } from './client.js';
|
|
9
|
+
export { verifyEntitlementJwt, remoteJwks, LicenseVerifyError, } from './jwks.js';
|
|
10
|
+
export { entitlePlugin } from './entitle.js';
|
|
11
|
+
export { EntitlementSchema, } from './types.js';
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,EAAE;AACF,+EAA+E;AAC/E,iFAAiF;AACjF,2EAA2E;AAC3E,iFAAiF;AACjF,qFAAqF;AAErF,OAAO,EACL,mBAAmB,GAGpB,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,oBAAoB,EACpB,UAAU,EACV,kBAAkB,GAEnB,MAAM,WAAW,CAAA;AAClB,OAAO,EAAE,aAAa,EAA6B,MAAM,cAAc,CAAA;AACvE,OAAO,EACL,iBAAiB,GAKlB,MAAM,YAAY,CAAA"}
|
package/dist/jwks.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type JWTVerifyGetKey } from 'jose';
|
|
2
|
+
import { type Entitlement } from './types.js';
|
|
3
|
+
export declare class LicenseVerifyError extends Error {
|
|
4
|
+
readonly code: 'jwks_fetch_failed' | 'invalid_token' | 'expired' | 'invalid_claims';
|
|
5
|
+
constructor(code: 'jwks_fetch_failed' | 'invalid_token' | 'expired' | 'invalid_claims', message: string);
|
|
6
|
+
}
|
|
7
|
+
export interface VerifyEntitlementJwtOptions {
|
|
8
|
+
/** JWKS endpoint, e.g. `https://nexus.example/api/licenses/jwks.json`, OR a pre-built jose key-getter. */
|
|
9
|
+
jwks: string | JWTVerifyGetKey;
|
|
10
|
+
/** Expected `iss` (e.g. `nexus.digitaleprojekte.at`). Enforced if set. */
|
|
11
|
+
issuer?: string;
|
|
12
|
+
/** Expected `aud` (e.g. `nexus`). Enforced if set. */
|
|
13
|
+
audience?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Build (and let the caller cache) a JWKS key-getter from a URL. */
|
|
16
|
+
export declare function remoteJwks(jwksUrl: string): JWTVerifyGetKey;
|
|
17
|
+
/**
|
|
18
|
+
* Verify a NEXUS agent-JWT against JWKS (alg-pinned EdDSA) and extract the
|
|
19
|
+
* entitlement claims. Throws LicenseVerifyError on any failure — there is no
|
|
20
|
+
* permissive return path (default-deny by construction).
|
|
21
|
+
*
|
|
22
|
+
* NOTE: pass a pre-built `remoteJwks(url)` getter (cached across calls) rather
|
|
23
|
+
* than a URL string in a hot path, so JWKS isn't re-fetched per verify.
|
|
24
|
+
*/
|
|
25
|
+
export declare function verifyEntitlementJwt(token: string, opts: VerifyEntitlementJwtOptions): Promise<Entitlement>;
|
|
26
|
+
//# sourceMappingURL=jwks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwks.d.ts","sourceRoot":"","sources":["../src/jwks.ts"],"names":[],"mappings":"AAQA,OAAO,EAAiC,KAAK,eAAe,EAAE,MAAM,MAAM,CAAA;AAC1E,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA;AAEhE,qBAAa,kBAAmB,SAAQ,KAAK;aAEzB,IAAI,EAAE,mBAAmB,GAAG,eAAe,GAAG,SAAS,GAAG,gBAAgB;gBAA1E,IAAI,EAAE,mBAAmB,GAAG,eAAe,GAAG,SAAS,GAAG,gBAAgB,EAC1F,OAAO,EAAE,MAAM;CAKlB;AAED,MAAM,WAAW,2BAA2B;IAC1C,0GAA0G;IAC1G,IAAI,EAAE,MAAM,GAAG,eAAe,CAAA;IAC9B,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,qEAAqE;AACrE,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,CAE3D;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,2BAA2B,GAChC,OAAO,CAAC,WAAW,CAAC,CA0CtB"}
|
package/dist/jwks.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Offline JWKS verification of a NEXUS agent-JWT → Entitlement.
|
|
2
|
+
//
|
|
3
|
+
// The host typically already verifies the agent-JWT (for app-licensing) and can
|
|
4
|
+
// pass the verified claims straight to createLicenseClient via resolveEntitlement.
|
|
5
|
+
// This helper is for callers that hold the raw JWT and want the Foundation to do
|
|
6
|
+
// the verify: alg-pinned EdDSA, JWKS-cached, optional iss/aud — default-deny by
|
|
7
|
+
// construction (any verify failure throws, never returns a permissive result).
|
|
8
|
+
import { jwtVerify, createRemoteJWKSet } from 'jose';
|
|
9
|
+
import { EntitlementSchema } from './types.js';
|
|
10
|
+
export class LicenseVerifyError extends Error {
|
|
11
|
+
code;
|
|
12
|
+
constructor(code, message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.name = 'LicenseVerifyError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Build (and let the caller cache) a JWKS key-getter from a URL. */
|
|
19
|
+
export function remoteJwks(jwksUrl) {
|
|
20
|
+
return createRemoteJWKSet(new URL(jwksUrl));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Verify a NEXUS agent-JWT against JWKS (alg-pinned EdDSA) and extract the
|
|
24
|
+
* entitlement claims. Throws LicenseVerifyError on any failure — there is no
|
|
25
|
+
* permissive return path (default-deny by construction).
|
|
26
|
+
*
|
|
27
|
+
* NOTE: pass a pre-built `remoteJwks(url)` getter (cached across calls) rather
|
|
28
|
+
* than a URL string in a hot path, so JWKS isn't re-fetched per verify.
|
|
29
|
+
*/
|
|
30
|
+
export async function verifyEntitlementJwt(token, opts) {
|
|
31
|
+
const keyGetter = typeof opts.jwks === 'string' ? remoteJwks(opts.jwks) : opts.jwks;
|
|
32
|
+
let payload;
|
|
33
|
+
try {
|
|
34
|
+
const result = await jwtVerify(token, keyGetter, {
|
|
35
|
+
algorithms: ['EdDSA'], // alg-pinned: never accept HS*/none/etc.
|
|
36
|
+
...(opts.issuer !== undefined ? { issuer: opts.issuer } : {}),
|
|
37
|
+
...(opts.audience !== undefined ? { audience: opts.audience } : {}),
|
|
38
|
+
});
|
|
39
|
+
payload = result.payload;
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
const e = err;
|
|
43
|
+
const msg = e.message ?? 'verify failed';
|
|
44
|
+
// Token-expiry first — most specific.
|
|
45
|
+
if (e.code === 'ERR_JWT_EXPIRED' || /expired/i.test(msg)) {
|
|
46
|
+
throw new LicenseVerifyError('expired', msg);
|
|
47
|
+
}
|
|
48
|
+
// JWKS unreachable / no key / timeout / transport — distinct from a bad token,
|
|
49
|
+
// so the host can retry-later instead of treating it as token-integrity.
|
|
50
|
+
if (e.code === 'ERR_JWKS_NO_MATCHING_KEY' ||
|
|
51
|
+
e.code === 'ERR_JWKS_TIMEOUT' ||
|
|
52
|
+
e.code === 'ERR_JWKS_MULTIPLE_MATCHING_KEYS' ||
|
|
53
|
+
/jwks|key set|fetch failed|timed out|network|ENOTFOUND|ECONNREFUSED/i.test(msg)) {
|
|
54
|
+
throw new LicenseVerifyError('jwks_fetch_failed', msg);
|
|
55
|
+
}
|
|
56
|
+
throw new LicenseVerifyError('invalid_token', msg);
|
|
57
|
+
}
|
|
58
|
+
const parsed = EntitlementSchema.safeParse(payload);
|
|
59
|
+
if (!parsed.success) {
|
|
60
|
+
throw new LicenseVerifyError('invalid_claims', `entitlement claims invalid: ${parsed.error.issues.map((i) => i.path.join('.')).join(', ')}`);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
plugins: parsed.data.plugins,
|
|
64
|
+
ent_ver: parsed.data.ent_ver,
|
|
65
|
+
...(parsed.data.exp !== undefined ? { exp: parsed.data.exp } : {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=jwks.js.map
|
package/dist/jwks.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwks.js","sourceRoot":"","sources":["../src/jwks.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** Entitlement payload extracted from a verified NEXUS agent-JWT. */
|
|
3
|
+
export interface Entitlement {
|
|
4
|
+
/** Entitled plugin slugs (the authoritative offline list mirror). */
|
|
5
|
+
plugins: string[];
|
|
6
|
+
/** Monotonic entitlements version (revocation / reconcile cursor). */
|
|
7
|
+
ent_ver: number;
|
|
8
|
+
/** JWT `exp` (unix seconds) — the claim's freshness boundary, if known. */
|
|
9
|
+
exp?: number;
|
|
10
|
+
}
|
|
11
|
+
/** Zod schema — parses the relevant claims off a verified JWT payload. Lenient
|
|
12
|
+
* about extra claims (the agent-JWT carries more than just entitlements). */
|
|
13
|
+
export declare const EntitlementSchema: z.ZodObject<{
|
|
14
|
+
plugins: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
15
|
+
ent_ver: z.ZodDefault<z.ZodNumber>;
|
|
16
|
+
exp: z.ZodNumber;
|
|
17
|
+
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
18
|
+
plugins: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
19
|
+
ent_ver: z.ZodDefault<z.ZodNumber>;
|
|
20
|
+
exp: z.ZodNumber;
|
|
21
|
+
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
22
|
+
plugins: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
23
|
+
ent_ver: z.ZodDefault<z.ZodNumber>;
|
|
24
|
+
exp: z.ZodNumber;
|
|
25
|
+
}, z.ZodTypeAny, "passthrough">>;
|
|
26
|
+
/**
|
|
27
|
+
* Why a license check failed (or `undefined` on success):
|
|
28
|
+
* - `not_entitled` — entitlement resolved, plugin not in `plugins[]`.
|
|
29
|
+
* - `no_entitlement` — could not resolve an entitlement and no valid cache (default-deny).
|
|
30
|
+
* - `stale_entitlement` — only a cached entitlement exists and it is past `exp` + grace.
|
|
31
|
+
* - `purchase_required` — entitle-on-activation returned 402 (paid, not purchased).
|
|
32
|
+
* - `entitlement_error` — resolving/parsing the entitlement threw.
|
|
33
|
+
*/
|
|
34
|
+
export type LicenseDenyReason = 'not_entitled' | 'no_entitlement' | 'stale_entitlement' | 'purchase_required' | 'entitlement_error';
|
|
35
|
+
export type LicenseGateResult = {
|
|
36
|
+
ok: true;
|
|
37
|
+
} | {
|
|
38
|
+
ok: false;
|
|
39
|
+
reason: LicenseDenyReason;
|
|
40
|
+
message?: string;
|
|
41
|
+
};
|
|
42
|
+
/** The seam `@theseus/plugin-system` `PluginManager.activate` calls before activation. */
|
|
43
|
+
export interface LicenseGate {
|
|
44
|
+
check(input: {
|
|
45
|
+
pluginId: string;
|
|
46
|
+
userId?: string;
|
|
47
|
+
manifest?: unknown;
|
|
48
|
+
}): Promise<LicenseGateResult>;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,qEAAqE;AACrE,MAAM,WAAW,WAAW;IAC1B,qEAAqE;IACrE,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAA;IACf,2EAA2E;IAC3E,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED;8EAC8E;AAC9E,eAAO,MAAM,iBAAiB;;;;;;;;;;;;gCASd,CAAA;AAEhB;;;;;;;GAOG;AACH,MAAM,MAAM,iBAAiB,GACzB,cAAc,GACd,gBAAgB,GAChB,mBAAmB,GACnB,mBAAmB,GACnB,mBAAmB,CAAA;AAEvB,MAAM,MAAM,iBAAiB,GACzB;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9D,0FAA0F;AAC1F,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,KAAK,EAAE;QACX,QAAQ,EAAE,MAAM,CAAA;QAChB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,QAAQ,CAAC,EAAE,OAAO,CAAA;KACnB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAA;CAC/B"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Plugin-License Foundation — shared types for the NEXUS entitlement model.
|
|
2
|
+
//
|
|
3
|
+
// Entitlements ride as signed claims on the NEXUS agent-JWT (verified offline
|
|
4
|
+
// via JWKS): `plugins: string[]` (entitled plugin slugs) + `ent_ver: int`
|
|
5
|
+
// (monotonic version, bumped on revocation → host reconciles via SSE). The host
|
|
6
|
+
// already verifies this JWT for app-licensing; this package adds the per-plugin
|
|
7
|
+
// LicenseGate + last-known-good offline grace + the entitle-on-activation call.
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
/** Zod schema — parses the relevant claims off a verified JWT payload. Lenient
|
|
10
|
+
* about extra claims (the agent-JWT carries more than just entitlements). */
|
|
11
|
+
export const EntitlementSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
plugins: z.array(z.string()).default([]),
|
|
14
|
+
ent_ver: z.number().int().nonnegative().default(0),
|
|
15
|
+
// REQUIRED: a verified NEXUS agent-JWT always carries `exp` (short TTL). An
|
|
16
|
+
// exp-less token → `invalid_claims` at verify, so the gate's offline grace is
|
|
17
|
+
// always anchored on a signed freshness boundary (closes the exp-less edge).
|
|
18
|
+
exp: z.number().int().positive(),
|
|
19
|
+
})
|
|
20
|
+
.passthrough();
|
|
21
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,8EAA8E;AAC9E,0EAA0E;AAC1E,gFAAgF;AAChF,gFAAgF;AAChF,gFAAgF;AAEhF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAYvB;8EAC8E;AAC9E,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC;KAC/B,MAAM,CAAC;IACN,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACxC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IAClD,4EAA4E;IAC5E,8EAA8E;IAC9E,6EAA6E;IAC7E,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;CACjC,CAAC;KACD,WAAW,EAAE,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nexus-mindgarden/plugin-license-foundation",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Plugin-License Foundation — NEXUS entitlement LicenseGate (offline JWKS-verified, default-deny, last-known-good grace) + entitle-on-activation",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/MrDewitt88/plugin-template.git",
|
|
25
|
+
"directory": "packages/plugin-license-foundation"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/MrDewitt88/plugin-template/tree/main/packages/plugin-license-foundation#readme",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/MrDewitt88/plugin-template/issues"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"nexus-mindgarden",
|
|
33
|
+
"plugin",
|
|
34
|
+
"license",
|
|
35
|
+
"entitlement",
|
|
36
|
+
"licensegate"
|
|
37
|
+
],
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"jose": "^5.9.0",
|
|
40
|
+
"zod": "^3.23.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^22.0.0",
|
|
44
|
+
"typescript": "^5.6.0",
|
|
45
|
+
"vitest": "^2.1.0"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc -p tsconfig.json",
|
|
49
|
+
"test": "vitest run",
|
|
50
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
51
|
+
}
|
|
52
|
+
}
|