@voltro/plugin-licensing 0.84.0 → 0.86.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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Effect } from 'effect';
2
- import { Tag } from 'effect/Context';
2
+ import { Service } from 'effect/Context';
3
3
  import { VoidIfEmpty } from 'effect/Types';
4
4
  import { VoltroPlugin } from '@voltro/protocol';
5
5
  import { YieldableError } from 'effect/Cause';
@@ -36,7 +36,7 @@ export declare interface LicenseClaims {
36
36
  readonly kid?: string;
37
37
  }
38
38
 
39
- export declare const LicenseService: Tag<LicenseServiceShape, LicenseServiceShape>;
39
+ export declare const LicenseService: Service<LicenseServiceShape, LicenseServiceShape>;
40
40
 
41
41
  export declare interface LicenseServiceShape {
42
42
  readonly snapshot: () => LicenseSnapshot | undefined;
@@ -119,10 +119,12 @@ export declare interface LicensingPluginOptions {
119
119
  readonly publicKey?: string;
120
120
  /** The signed license token. Falls back to `VOLTRO_LICENSE_KEY`. */
121
121
  readonly licenseKey?: string;
122
- /** URL to periodically re-fetch a fresh signed license (entitlement sync).
123
- * With `refreshMs`, a cluster-coordinated refresh re-verifies + re-installs
124
- * the snapshot. (The cloud endpoint that serves it ships in a later milestone.) */
122
+ /** URL returning JSON containing a signed `token`. The coordinated refresh
123
+ * verifies its signature before installing the new snapshot. */
125
124
  readonly snapshotUrl?: string;
125
+ /** Server-only snapshot headers, for example a service Authorization header.
126
+ * Redirects are refused. Keep credentials out of browsers and URLs. */
127
+ readonly snapshotHeaders?: Readonly<Record<string, string>>;
126
128
  /** Refresh interval (ms) for `snapshotUrl`. Default 15 minutes. */
127
129
  readonly refreshMs?: number;
128
130
  /** Seconds an expired license is still honored (grace) before it goes dark.
package/dist/index.js CHANGED
@@ -1,84 +1,18 @@
1
- import { Data as e, Effect as t } from "effect";
2
- import { definePlugin as n, definePluginService as r, pluginInstanceName as i } from "@voltro/protocol";
3
- import { pluginEnv as a } from "@voltro/env";
4
- import { importSPKI as o, jwtVerify as s } from "jose";
5
- //#region src/verify.ts
6
- var c = class extends e.TaggedError("LicenseVerifyError") {}, l = (e) => typeof e == "object" && e && "code" in e ? String(e.code) : "", u = (e) => {
7
- let t = e instanceof Error ? e.message : String(e);
8
- return l(e) === "ERR_JWT_EXPIRED" ? new c({
9
- reason: "expired",
10
- message: t
11
- }) : new c({
12
- reason: "invalid",
13
- message: t
14
- });
15
- }, d = (e) => e === "unlimited" || typeof e == "number" && Number.isFinite(e) && e >= 0, f = (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 c({
18
- reason: "malformed",
19
- message: "missing edition/entitlements"
20
- }));
21
- let o = {};
22
- for (let [e, n] of Object.entries(i)) {
23
- if (!d(n)) return t.fail(new c({
24
- reason: "malformed",
25
- message: `bad entitlement '${e}'`
26
- }));
27
- o[e] = n;
28
- }
29
- let s = Array.isArray(a) ? a.filter((e) => typeof e == "string") : [], l = e.plugins, u = 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: s,
35
- plugins: u,
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
- }, p = (e, n) => t.tryPromise({
41
- try: () => o(n, "EdDSA"),
42
- catch: (e) => new c({
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 s(e, n, { algorithms: ["EdDSA"] });
49
- return {
50
- payload: t,
51
- kid: r.kid
52
- };
53
- },
54
- catch: u
55
- })), t.flatMap(({ payload: e, kid: t }) => f(e, t))), m = (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
- state: () => e.exp <= 0 || a() <= e.exp ? "active" : o() ? "grace" : "expired",
60
- limitFor: (t, n) => {
61
- if (!o() || e.tenant !== void 0 && e.tenant !== t) return;
62
- let r = e.entitlements[n];
63
- if (r !== void 0) return r === "unlimited" ? Infinity : r;
64
- },
65
- hasFeature: (e) => o() && n.has(e),
66
- allowsPlugin: (e) => o() && r.has(e)
67
- };
68
- }, h, g = (e) => {
69
- h = e;
70
- }, _ = () => h, v, y = (e) => {
71
- v = e;
72
- }, b = () => v, x = (e, n) => t.sync(() => {
73
- let t = _()?.limitFor(e, n);
74
- return t === void 0 ? null : t;
75
- }), S = () => {
76
- let e = _(), t = e === void 0 ? void 0 : {
1
+ import { fetchLicenseSnapshot as e } from "./refresh.js";
2
+ import { LicenseVerifyError as t, verifyLicense as n } from "./verify.js";
3
+ import { getRejection as r, getSnapshot as i, makeSnapshot as a, setRejection as o, setSnapshot as s } from "./snapshot.js";
4
+ import { entitlementResolver as c } from "./resolver.js";
5
+ import { Data as l, Effect as u } from "effect";
6
+ import { definePlugin as d, definePluginService as f, pluginInstanceName as p } from "@voltro/protocol";
7
+ import { pluginEnv as m } from "@voltro/env";
8
+ //#region src/index.ts
9
+ var h = () => {
10
+ let e = i(), t = e === void 0 ? void 0 : {
77
11
  state: e.state(),
78
12
  edition: e.claims.edition,
79
13
  expiresAt: e.claims.exp,
80
14
  ...e.claims.tenant === void 0 ? {} : { tenant: e.claims.tenant }
81
- }, n = b();
15
+ }, n = r();
82
16
  return n === void 0 ? t ?? { state: "none" } : {
83
17
  state: "rejected",
84
18
  reason: n.reason,
@@ -89,7 +23,7 @@ var c = class extends e.TaggedError("LicenseVerifyError") {}, l = (e) => typeof
89
23
  expiresAt: t.expiresAt
90
24
  } }
91
25
  };
92
- }, C = a([{
26
+ }, g = m([{
93
27
  name: "VOLTRO_LICENSE_PUBLIC_KEY",
94
28
  required: !1,
95
29
  secret: !1,
@@ -99,66 +33,59 @@ var c = class extends e.TaggedError("LicenseVerifyError") {}, l = (e) => typeof
99
33
  required: !1,
100
34
  secret: !1,
101
35
  description: "The signed EdDSA license token, verified offline at boot."
102
- }]), { Tag: w, Live: T } = r("@voltro/plugin-licensing/LicenseService", {
103
- snapshot: () => _(),
104
- status: () => S(),
105
- limitFor: (e, t) => _()?.limitFor(e, t),
106
- hasFeature: (e) => _()?.hasFeature(e) ?? !1,
107
- allowsPlugin: (e) => _()?.allowsPlugin(e) ?? !1,
108
- edition: () => _()?.claims.edition
109
- }), E = class extends e.TaggedError("PluginNotLicensed") {}, D = (e) => _()?.allowsPlugin(e) ?? !1, O = (e) => D(e) ? t.void : t.fail(new E({ plugin: e })), k = (e = {}) => {
110
- let r = C.read("VOLTRO_LICENSE_PUBLIC_KEY", e.publicKey), a = C.read("VOLTRO_LICENSE_KEY", e.licenseKey), o = e.graceSeconds ?? 0, s = e.refreshMs ?? 9e5, c, l = (e, n) => p(e, n).pipe(t.match({
36
+ }]), { Tag: _, Live: v } = f("@voltro/plugin-licensing/LicenseService", {
37
+ snapshot: () => i(),
38
+ status: () => h(),
39
+ limitFor: (e, t) => i()?.limitFor(e, t),
40
+ hasFeature: (e) => i()?.hasFeature(e) ?? !1,
41
+ allowsPlugin: (e) => i()?.allowsPlugin(e) ?? !1,
42
+ edition: () => i()?.claims.edition
43
+ }), y = class extends l.TaggedError("PluginNotLicensed") {}, b = (e) => i()?.allowsPlugin(e) ?? !1, x = (e) => b(e) ? u.void : u.fail(new y({ plugin: e })), S = (t = {}) => {
44
+ let r = g.read("VOLTRO_LICENSE_PUBLIC_KEY", t.publicKey), i = g.read("VOLTRO_LICENSE_KEY", t.licenseKey), c = t.graceSeconds ?? 0, l = t.refreshMs ?? 9e5, f, m = (e, t) => n(e, t).pipe(u.match({
111
45
  onSuccess: (e) => {
112
- g(m(e, { graceSeconds: o })), y(void 0), c?.info("license active", {
46
+ s(a(e, { graceSeconds: c })), o(void 0), f?.info("license active", {
113
47
  edition: e.edition,
114
48
  entitlements: Object.keys(e.entitlements).length,
115
49
  exp: e.exp
116
50
  });
117
51
  },
118
52
  onFailure: (e) => {
119
- y({
53
+ o({
120
54
  reason: e.reason,
121
55
  message: e.message
122
- }), c?.warn("license verification failed — entitlements fall back to static plan", {
56
+ }), f?.warn("license verification failed — entitlements fall back to static plan", {
123
57
  reason: e.reason,
124
58
  message: e.message
125
59
  });
126
60
  }
127
61
  }));
128
- return n({
129
- name: i({
62
+ return d({
63
+ name: p({
130
64
  base: "@voltro/plugin-licensing",
131
- instance: e.name
65
+ instance: t.name
132
66
  }),
133
67
  description: "Offline-verified EdDSA license keys + entitlement snapshots; feeds @voltro/plugin-billing via entitlementResolver.",
134
- declaredEnv: C.declared,
135
- services: T,
136
- onActivate: (e) => t.gen(function* () {
137
- if (c = e.logger, r === void 0 || a === void 0) {
138
- e.logger.debug("licensing: no license configured — static plan entitlements apply");
139
- return;
140
- }
141
- yield* l(a, r);
68
+ declaredEnv: g.declared,
69
+ services: v,
70
+ onActivate: (e) => u.gen(function* () {
71
+ f = e.logger, r === void 0 || i === void 0 ? e.logger.debug("licensing: no license configured — static plan entitlements apply") : yield* m(i, r);
142
72
  }),
143
73
  bindDataStore: (n, i) => {
144
- let a = e.snapshotUrl;
74
+ let a = t.snapshotUrl;
145
75
  if (a === void 0 || r === void 0 || i?.scheduleCoordinated === void 0) return;
146
76
  let o = r;
147
- i.scheduleCoordinated("licensing.refresh", s, async () => {
148
- try {
149
- let e = await fetch(a);
150
- if (!e.ok) return;
151
- let n = (await e.text()).trim();
152
- n.length > 0 && await t.runPromise(l(n, o));
153
- } catch (e) {
154
- c?.warn("license refresh failed", { message: e instanceof Error ? e.message : String(e) });
155
- }
77
+ i.scheduleCoordinated("licensing.refresh", l, async () => {
78
+ let n = await e(a, t.snapshotHeaders);
79
+ n.kind === "token" ? await u.runPromise(m(n.token, o)) : f?.warn("license refresh failed — retaining the last verified snapshot", {
80
+ reason: n.reason,
81
+ ...n.reason === "http" ? { status: n.status } : {}
82
+ });
156
83
  });
157
84
  },
158
- onDeactivate: () => t.sync(() => {
159
- g(void 0), y(void 0);
85
+ onDeactivate: () => u.sync(() => {
86
+ s(void 0), o(void 0);
160
87
  })
161
88
  });
162
89
  };
163
90
  //#endregion
164
- export { w as LicenseService, c as LicenseVerifyError, E as PluginNotLicensed, x as entitlementResolver, D as licenseAllowsPlugin, S as licenseStatus, k as licensingPlugin, O as requireLicensedPlugin, p as verifyLicense };
91
+ export { _ as LicenseService, t as LicenseVerifyError, y as PluginNotLicensed, c as entitlementResolver, b as licenseAllowsPlugin, h as licenseStatus, S as licensingPlugin, x as requireLicensedPlugin, n as verifyLicense };
@@ -0,0 +1,57 @@
1
+ //#region src/refresh.ts
2
+ var e = async (e, t = {}) => {
3
+ try {
4
+ let n = await fetch(e, {
5
+ headers: t,
6
+ redirect: "error",
7
+ signal: AbortSignal.timeout(1e4)
8
+ });
9
+ if (!n.ok) return await n.body?.cancel(), {
10
+ kind: "refused",
11
+ reason: "http",
12
+ status: n.status
13
+ };
14
+ if (n.body === null) return {
15
+ kind: "refused",
16
+ reason: "response"
17
+ };
18
+ let r = n.body.getReader(), i = new TextDecoder("utf-8", { fatal: !0 }), a = 0, o = "";
19
+ try {
20
+ for (;;) {
21
+ let e = await r.read();
22
+ if (e.done) break;
23
+ if (a += e.value.byteLength, a > 65536) return await r.cancel(), {
24
+ kind: "refused",
25
+ reason: "response"
26
+ };
27
+ o += i.decode(e.value, { stream: !0 });
28
+ }
29
+ o += i.decode();
30
+ } finally {
31
+ r.releaseLock();
32
+ }
33
+ let s;
34
+ try {
35
+ s = JSON.parse(o);
36
+ } catch {
37
+ return {
38
+ kind: "refused",
39
+ reason: "response"
40
+ };
41
+ }
42
+ return typeof s != "object" || !s || Array.isArray(s) || !("token" in s) || typeof s.token != "string" || s.token.trim() === "" ? {
43
+ kind: "refused",
44
+ reason: "response"
45
+ } : {
46
+ kind: "token",
47
+ token: s.token
48
+ };
49
+ } catch {
50
+ return {
51
+ kind: "refused",
52
+ reason: "transport"
53
+ };
54
+ }
55
+ };
56
+ //#endregion
57
+ export { e as fetchLicenseSnapshot };
@@ -0,0 +1,9 @@
1
+ import { getSnapshot as e } from "./snapshot.js";
2
+ import { Effect as t } from "effect";
3
+ //#region src/resolver.ts
4
+ var n = (n, r) => t.sync(() => {
5
+ let t = e()?.limitFor(n, r);
6
+ return t === void 0 ? null : t;
7
+ });
8
+ //#endregion
9
+ export { n as entitlementResolver };
@@ -0,0 +1,21 @@
1
+ //#region src/snapshot.ts
2
+ var e = (e, t = {}) => {
3
+ 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;
4
+ return {
5
+ claims: e,
6
+ state: () => e.exp <= 0 || a() <= e.exp ? "active" : o() ? "grace" : "expired",
7
+ limitFor: (t, n) => {
8
+ if (!o() || e.tenant !== void 0 && e.tenant !== t) return;
9
+ let r = e.entitlements[n];
10
+ if (r !== void 0) return r === "unlimited" ? Infinity : r;
11
+ },
12
+ hasFeature: (e) => o() && n.has(e),
13
+ allowsPlugin: (e) => o() && r.has(e)
14
+ };
15
+ }, t, n = (e) => {
16
+ t = e;
17
+ }, r = () => t, i, a = (e) => {
18
+ i = e;
19
+ }, o = () => i;
20
+ //#endregion
21
+ export { o as getRejection, r as getSnapshot, e as makeSnapshot, a as setRejection, n as setSnapshot };
package/dist/verify.js ADDED
@@ -0,0 +1,55 @@
1
+ import { Data as e, Effect as t } from "effect";
2
+ import { importSPKI as n, jwtVerify as r } from "jose";
3
+ //#region src/verify.ts
4
+ var i = class extends e.TaggedError("LicenseVerifyError") {}, a = (e) => typeof e == "object" && e && "code" in e ? String(e.code) : "", o = (e) => {
5
+ let t = e instanceof Error ? e.message : String(e);
6
+ return a(e) === "ERR_JWT_EXPIRED" ? new i({
7
+ reason: "expired",
8
+ message: t
9
+ }) : new i({
10
+ reason: "invalid",
11
+ message: t
12
+ });
13
+ }, s = (e) => e === "unlimited" || typeof e == "number" && Number.isFinite(e) && e >= 0, c = (e, n) => {
14
+ let r = e.edition, a = e.entitlements, o = e.features;
15
+ if (typeof r != "string" || typeof a != "object" || !a) return t.fail(new i({
16
+ reason: "malformed",
17
+ message: "missing edition/entitlements"
18
+ }));
19
+ let c = {};
20
+ for (let [e, n] of Object.entries(a)) {
21
+ if (!s(n)) return t.fail(new i({
22
+ reason: "malformed",
23
+ message: `bad entitlement '${e}'`
24
+ }));
25
+ c[e] = n;
26
+ }
27
+ let l = Array.isArray(o) ? o.filter((e) => typeof e == "string") : [], u = e.plugins, d = Array.isArray(u) ? u.filter((e) => typeof e == "string") : [];
28
+ return t.succeed({
29
+ ...typeof e.tenant == "string" ? { tenant: e.tenant } : {},
30
+ edition: r,
31
+ entitlements: c,
32
+ features: l,
33
+ plugins: d,
34
+ exp: typeof e.exp == "number" ? e.exp : 0,
35
+ ...typeof e.iat == "number" ? { iat: e.iat } : {},
36
+ ...n === void 0 ? {} : { kid: n }
37
+ });
38
+ }, l = (e, a) => t.tryPromise({
39
+ try: () => n(a, "EdDSA"),
40
+ catch: (e) => new i({
41
+ reason: "key",
42
+ message: e instanceof Error ? e.message : String(e)
43
+ })
44
+ }).pipe(t.flatMap((n) => t.tryPromise({
45
+ try: async () => {
46
+ let { payload: t, protectedHeader: i } = await r(e, n, { algorithms: ["EdDSA"] });
47
+ return {
48
+ payload: t,
49
+ kid: i.kid
50
+ };
51
+ },
52
+ catch: o
53
+ })), t.flatMap(({ payload: e, kid: t }) => c(e, t)));
54
+ //#endregion
55
+ export { i as LicenseVerifyError, l as verifyLicense };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/plugin-licensing",
3
- "version": "0.84.0",
3
+ "version": "0.86.0",
4
4
  "description": "Offline-verified license keys + cloud-issued entitlement snapshots for @voltro apps",
5
5
  "keywords": [
6
6
  "voltro",
@@ -38,14 +38,14 @@
38
38
  "node": ">=24.0.0"
39
39
  },
40
40
  "dependencies": {
41
- "@voltro/database": "0.84.0",
42
- "@voltro/env": "0.84.0",
43
- "@voltro/logger": "0.84.0",
44
- "@voltro/protocol": "0.84.0",
41
+ "@voltro/database": "0.86.0",
42
+ "@voltro/env": "0.86.0",
43
+ "@voltro/logger": "0.86.0",
44
+ "@voltro/protocol": "0.86.0",
45
45
  "jose": "^6.2.12"
46
46
  },
47
47
  "peerDependencies": {
48
- "effect": "^3.22.2"
48
+ "effect": "4.0.1"
49
49
  },
50
50
  "publishConfig": {
51
51
  "access": "public"