@voltro/plugin-licensing 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.
@@ -0,0 +1,122 @@
1
+ import { Effect } from 'effect';
2
+ import { Tag } from 'effect/Context';
3
+ import { VoidIfEmpty } from 'effect/Types';
4
+ import { VoltroPlugin } from '@voltro/protocol';
5
+ import { YieldableError } from 'effect/Cause';
6
+
7
+ /** A numeric cap, or `'unlimited'` for no cap. */
8
+ export declare type EntitlementLimit = number | 'unlimited';
9
+
10
+ export declare const entitlementResolver: (tenantId: string | null | undefined, key: string) => Effect.Effect<number | null>;
11
+
12
+ /** Whether the current license grants `plugin` (name without the
13
+ * `@voltro/plugin-` prefix) for production use. Reads the in-memory snapshot;
14
+ * no license / no grant → false (fail-closed). */
15
+ export declare const licenseAllowsPlugin: (plugin: string) => boolean;
16
+
17
+ /** The entitlement + feature grant a license carries. */
18
+ export declare interface LicenseClaims {
19
+ /** If set, the license applies ONLY to this tenant id; unset = deployment-wide. */
20
+ readonly tenant?: string;
21
+ /** Edition label (e.g. `'pro'`, `'enterprise'`). */
22
+ readonly edition: string;
23
+ /** Per-key numeric limits. */
24
+ readonly entitlements: Readonly<Record<string, EntitlementLimit>>;
25
+ /** Boolean feature flags granted. */
26
+ readonly features: ReadonlyArray<string>;
27
+ /** Plugins licensed for production use (the "installed ≠ billable" gate —
28
+ * a plugin can be present in the bundle but only run under a license grant).
29
+ * Names without the `@voltro/plugin-` prefix, e.g. `['storage','search']`. */
30
+ readonly plugins: ReadonlyArray<string>;
31
+ /** Unix-seconds expiry (from the JWT `exp`). */
32
+ readonly exp: number;
33
+ /** Unix-seconds issued-at (from the JWT `iat`), if present. */
34
+ readonly iat?: number;
35
+ /** Key id from the JWS header — supports public-key rotation. */
36
+ readonly kid?: string;
37
+ }
38
+
39
+ export declare const LicenseService: Tag<LicenseServiceShape, LicenseServiceShape>;
40
+
41
+ /** The service handlers can `yield*` to read the current license directly (for
42
+ * feature checks beyond billing's metered entitlements). */
43
+ export declare interface LicenseServiceShape {
44
+ readonly snapshot: () => LicenseSnapshot | undefined;
45
+ readonly limitFor: (tenantId: string | null | undefined, key: string) => number | undefined;
46
+ readonly hasFeature: (feature: string) => boolean;
47
+ readonly allowsPlugin: (plugin: string) => boolean;
48
+ readonly edition: () => string | undefined;
49
+ }
50
+
51
+ export declare interface LicenseSnapshot {
52
+ readonly claims: LicenseClaims;
53
+ /**
54
+ * Numeric limit for a `(tenant, key)` pair: a finite cap, `Infinity` for an
55
+ * `'unlimited'` entitlement, or `undefined` when this license doesn't cover
56
+ * the key (or is scoped to a different tenant) — the caller then falls back to
57
+ * its own defaults (billing's static plan registry).
58
+ */
59
+ readonly limitFor: (tenantId: string | null | undefined, key: string) => number | undefined;
60
+ /** Whether a boolean feature is granted by the license. */
61
+ readonly hasFeature: (feature: string) => boolean;
62
+ /** Whether a plugin is licensed for production use (name without the
63
+ * `@voltro/plugin-` prefix, e.g. `'storage'`). The "installed ≠ billable" gate. */
64
+ readonly allowsPlugin: (plugin: string) => boolean;
65
+ }
66
+
67
+ export declare class LicenseVerifyError extends LicenseVerifyError_base<{
68
+ /** `expired` — past `exp`; `invalid` — bad signature; `key` — unusable public
69
+ * key; `malformed` — verified but the payload isn't a license. */
70
+ readonly reason: 'expired' | 'invalid' | 'key' | 'malformed';
71
+ readonly message: string;
72
+ }> {
73
+ }
74
+
75
+ declare const LicenseVerifyError_base: new <A extends Record<string, any> = {}>(args: VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => YieldableError & {
76
+ readonly _tag: "LicenseVerifyError";
77
+ } & Readonly<A>;
78
+
79
+ export declare const licensingPlugin: (options?: LicensingPluginOptions) => VoltroPlugin;
80
+
81
+ export declare interface LicensingPluginOptions {
82
+ /** Ed25519 public key (SPKI/PEM) that verifies the license. Falls back to
83
+ * `VOLTRO_LICENSE_PUBLIC_KEY`. Safe to embed — it is a PUBLIC key. */
84
+ readonly publicKey?: string;
85
+ /** The signed license token. Falls back to `VOLTRO_LICENSE_KEY`. */
86
+ readonly licenseKey?: string;
87
+ /** URL to periodically re-fetch a fresh signed license (entitlement sync).
88
+ * With `refreshMs`, a cluster-coordinated refresh re-verifies + re-installs
89
+ * the snapshot. (The cloud endpoint that serves it ships in a later milestone.) */
90
+ readonly snapshotUrl?: string;
91
+ /** Refresh interval (ms) for `snapshotUrl`. Default 15 minutes. */
92
+ readonly refreshMs?: number;
93
+ /** Seconds an expired license is still honored (grace) before it goes dark.
94
+ * Default 0. */
95
+ readonly graceSeconds?: number;
96
+ /** Alias suffix when running more than one instance. */
97
+ readonly name?: string;
98
+ }
99
+
100
+ /** Thrown when a plugin's production use requires a license grant the current
101
+ * license doesn't carry (the "installed ≠ billable" gate). */
102
+ export declare class PluginNotLicensed extends PluginNotLicensed_base<{
103
+ readonly plugin: string;
104
+ }> {
105
+ }
106
+
107
+ declare const PluginNotLicensed_base: new <A extends Record<string, any> = {}>(args: VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => YieldableError & {
108
+ readonly _tag: "PluginNotLicensed";
109
+ } & Readonly<A>;
110
+
111
+ /** Gate a handler (or a plugin interceptor) on a plugin being licensed. Fails
112
+ * `PluginNotLicensed` when it isn't. */
113
+ export declare const requireLicensedPlugin: (plugin: string) => Effect.Effect<void, PluginNotLicensed>;
114
+
115
+ /**
116
+ * Verify a license token against an Ed25519 public key (SPKI/PEM), OFFLINE.
117
+ * Returns the decoded {@link LicenseClaims} or a typed {@link LicenseVerifyError}
118
+ * (expiry is enforced by `jose`). No network access.
119
+ */
120
+ export declare const verifyLicense: (token: string, publicKeyPem: string) => Effect.Effect<LicenseClaims, LicenseVerifyError>;
121
+
122
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,135 @@
1
+ import { Data as e, Effect as t } from "effect";
2
+ import { definePlugin as n, definePluginService as r } from "@voltro/protocol";
3
+ import { pluginEnv as i } from "@voltro/env";
4
+ import { importSPKI as a, jwtVerify as o } from "jose";
5
+ //#region src/verify.ts
6
+ var s = class extends e.TaggedError("LicenseVerifyError") {}, c = (e) => typeof e == "object" && e && "code" in e ? String(e.code) : "", l = (e) => {
7
+ let t = e instanceof Error ? e.message : String(e);
8
+ return c(e) === "ERR_JWT_EXPIRED" ? new s({
9
+ reason: "expired",
10
+ message: t
11
+ }) : new s({
12
+ reason: "invalid",
13
+ message: t
14
+ });
15
+ }, u = (e) => e === "unlimited" || typeof e == "number" && Number.isFinite(e) && e >= 0, d = (e, n) => {
16
+ let r = e.edition, i = e.entitlements, a = e.features;
17
+ if (typeof r != "string" || typeof i != "object" || !i) return t.fail(new s({
18
+ reason: "malformed",
19
+ message: "missing edition/entitlements"
20
+ }));
21
+ let o = {};
22
+ for (let [e, n] of Object.entries(i)) {
23
+ if (!u(n)) return t.fail(new s({
24
+ reason: "malformed",
25
+ message: `bad entitlement '${e}'`
26
+ }));
27
+ o[e] = n;
28
+ }
29
+ let c = Array.isArray(a) ? a.filter((e) => typeof e == "string") : [], l = e.plugins, d = Array.isArray(l) ? l.filter((e) => typeof e == "string") : [];
30
+ return t.succeed({
31
+ ...typeof e.tenant == "string" ? { tenant: e.tenant } : {},
32
+ edition: r,
33
+ entitlements: o,
34
+ features: c,
35
+ plugins: d,
36
+ exp: typeof e.exp == "number" ? e.exp : 0,
37
+ ...typeof e.iat == "number" ? { iat: e.iat } : {},
38
+ ...n === void 0 ? {} : { kid: n }
39
+ });
40
+ }, f = (e, n) => t.tryPromise({
41
+ try: () => a(n, "EdDSA"),
42
+ catch: (e) => new s({
43
+ reason: "key",
44
+ message: e instanceof Error ? e.message : String(e)
45
+ })
46
+ }).pipe(t.flatMap((n) => t.tryPromise({
47
+ try: async () => {
48
+ let { payload: t, protectedHeader: r } = await o(e, n, { algorithms: ["EdDSA"] });
49
+ return {
50
+ payload: t,
51
+ kid: r.kid
52
+ };
53
+ },
54
+ catch: l
55
+ })), t.flatMap(({ payload: e, kid: t }) => d(e, t))), p = (e, t = {}) => {
56
+ let n = new Set(e.features), r = new Set(e.plugins), i = t.graceSeconds ?? 0, a = t.now ?? (() => Date.now() / 1e3), o = () => e.exp <= 0 || a() <= e.exp + i;
57
+ return {
58
+ claims: e,
59
+ limitFor: (t, n) => {
60
+ if (!o() || e.tenant !== void 0 && e.tenant !== t) return;
61
+ let r = e.entitlements[n];
62
+ if (r !== void 0) return r === "unlimited" ? Infinity : r;
63
+ },
64
+ hasFeature: (e) => o() && n.has(e),
65
+ allowsPlugin: (e) => o() && r.has(e)
66
+ };
67
+ }, m, h = (e) => {
68
+ m = e;
69
+ }, g = () => m, _ = (e, n) => t.sync(() => {
70
+ let t = g()?.limitFor(e, n);
71
+ return t === void 0 ? null : t;
72
+ }), v = i([{
73
+ name: "VOLTRO_LICENSE_PUBLIC_KEY",
74
+ required: !1,
75
+ secret: !1,
76
+ description: "Ed25519 public key (SPKI/PEM) that verifies license keys offline."
77
+ }, {
78
+ name: "VOLTRO_LICENSE_KEY",
79
+ required: !1,
80
+ secret: !1,
81
+ description: "The signed EdDSA license token, verified offline at boot."
82
+ }]), { Tag: y, Live: b } = r("@voltro/plugin-licensing/LicenseService", {
83
+ snapshot: () => g(),
84
+ limitFor: (e, t) => g()?.limitFor(e, t),
85
+ hasFeature: (e) => g()?.hasFeature(e) ?? !1,
86
+ allowsPlugin: (e) => g()?.allowsPlugin(e) ?? !1,
87
+ edition: () => g()?.claims.edition
88
+ }), x = class extends e.TaggedError("PluginNotLicensed") {}, S = (e) => g()?.allowsPlugin(e) ?? !1, C = (e) => S(e) ? t.void : t.fail(new x({ plugin: e })), w = (e = {}) => {
89
+ let r = v.read("VOLTRO_LICENSE_PUBLIC_KEY", e.publicKey), i = v.read("VOLTRO_LICENSE_KEY", e.licenseKey), a = e.graceSeconds ?? 0, o = e.refreshMs ?? 15 * 6e4, s, c = (e, n) => f(e, n).pipe(t.match({
90
+ onSuccess: (e) => {
91
+ h(p(e, { graceSeconds: a })), s?.info("license active", {
92
+ edition: e.edition,
93
+ entitlements: Object.keys(e.entitlements).length,
94
+ exp: e.exp
95
+ });
96
+ },
97
+ onFailure: (e) => {
98
+ s?.warn("license verification failed — entitlements fall back to static plan", {
99
+ reason: e.reason,
100
+ message: e.message
101
+ });
102
+ }
103
+ }));
104
+ return n({
105
+ name: e.name ? `@voltro/plugin-licensing#${e.name}` : "@voltro/plugin-licensing",
106
+ description: "Offline-verified EdDSA license keys + entitlement snapshots; feeds @voltro/plugin-billing via entitlementResolver.",
107
+ declaredEnv: v.declared,
108
+ services: b,
109
+ onActivate: (e) => t.gen(function* () {
110
+ if (s = e.logger, r === void 0 || i === void 0) {
111
+ e.logger.debug("licensing: no license configured — static plan entitlements apply");
112
+ return;
113
+ }
114
+ yield* c(i, r);
115
+ }),
116
+ bindDataStore: (n, i) => {
117
+ let a = e.snapshotUrl;
118
+ if (a === void 0 || r === void 0 || i?.scheduleCoordinated === void 0) return;
119
+ let l = r;
120
+ i.scheduleCoordinated("licensing.refresh", o, async () => {
121
+ try {
122
+ let e = await fetch(a);
123
+ if (!e.ok) return;
124
+ let n = (await e.text()).trim();
125
+ n.length > 0 && await t.runPromise(c(n, l));
126
+ } catch (e) {
127
+ s?.warn("license refresh failed", { message: e instanceof Error ? e.message : String(e) });
128
+ }
129
+ });
130
+ },
131
+ onDeactivate: () => t.sync(() => h(void 0))
132
+ });
133
+ };
134
+ //#endregion
135
+ export { y as LicenseService, s as LicenseVerifyError, x as PluginNotLicensed, _ as entitlementResolver, S as licenseAllowsPlugin, w as licensingPlugin, C as requireLicensedPlugin, f as verifyLicense };
package/dist/sign.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ /** A numeric cap, or `'unlimited'` for no cap. */
2
+ declare type EntitlementLimit = number | 'unlimited';
3
+
4
+ /**
5
+ * Sign a license token with an Ed25519 private key (PKCS8/PEM). Returns a compact
6
+ * EdDSA JWT that {@link verifyLicense} checks offline. `nowSeconds` is injectable
7
+ * for deterministic tests.
8
+ */
9
+ export declare const signLicense: (input: SignLicenseInput, privateKeyPem: string, nowSeconds?: number) => Promise<string>;
10
+
11
+ export declare interface SignLicenseInput {
12
+ /** Scope the license to one tenant id; omit for a deployment-wide license. */
13
+ readonly tenant?: string;
14
+ /** Edition label (e.g. `'pro'`, `'enterprise'`). */
15
+ readonly edition: string;
16
+ /** Per-key numeric limits (`'unlimited'` for no cap). */
17
+ readonly entitlements: Readonly<Record<string, EntitlementLimit>>;
18
+ /** Boolean feature flags to grant. */
19
+ readonly features?: ReadonlyArray<string>;
20
+ /** Plugins licensed for production use (names without the
21
+ * `@voltro/plugin-` prefix, e.g. `['storage']`). */
22
+ readonly plugins?: ReadonlyArray<string>;
23
+ /** Seconds from `now` until the license expires. */
24
+ readonly ttlSeconds: number;
25
+ /** Key id stamped into the JWS header (public-key rotation). */
26
+ readonly kid?: string;
27
+ }
28
+
29
+ export { }
package/dist/sign.js ADDED
@@ -0,0 +1,17 @@
1
+ import { SignJWT as e, importPKCS8 as t } from "jose";
2
+ //#region src/sign.ts
3
+ var n = async (n, r, i) => {
4
+ let a = await t(r, "EdDSA"), o = i ?? Math.floor(Date.now() / 1e3);
5
+ return new e({
6
+ ...n.tenant === void 0 ? {} : { tenant: n.tenant },
7
+ edition: n.edition,
8
+ entitlements: n.entitlements,
9
+ features: n.features ?? [],
10
+ plugins: n.plugins ?? []
11
+ }).setProtectedHeader({
12
+ alg: "EdDSA",
13
+ ...n.kid === void 0 ? {} : { kid: n.kid }
14
+ }).setIssuedAt(o).setExpirationTime(o + n.ttlSeconds).sign(a);
15
+ };
16
+ //#endregion
17
+ export { n as signLicense };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@voltro/plugin-licensing",
3
+ "version": "0.1.0",
4
+ "description": "Offline-verified license keys + cloud-issued entitlement snapshots for @voltro apps",
5
+ "keywords": [
6
+ "voltro",
7
+ "typescript",
8
+ "framework"
9
+ ],
10
+ "license": "SEE LICENSE IN LICENSE",
11
+ "homepage": "https://voltro.dev",
12
+ "bugs": {
13
+ "email": "support@voltro.dev"
14
+ },
15
+ "author": {
16
+ "name": "Voltro UG",
17
+ "url": "https://voltro.dev"
18
+ },
19
+ "type": "module",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "./sign": {
27
+ "types": "./dist/sign.d.ts",
28
+ "import": "./dist/sign.js",
29
+ "default": "./dist/sign.js"
30
+ }
31
+ },
32
+ "main": "./dist/index.js",
33
+ "module": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "sideEffects": false,
36
+ "engines": {
37
+ "node": ">=24.0.0"
38
+ },
39
+ "dependencies": {
40
+ "@voltro/database": "0.1.0",
41
+ "@voltro/env": "0.1.0",
42
+ "@voltro/logger": "0.1.0",
43
+ "@voltro/protocol": "0.1.0",
44
+ "jose": "^6.2.3"
45
+ },
46
+ "peerDependencies": {
47
+ "effect": "^3.21.4"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ }
52
+ }