@middag-io/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.
- package/README.md +72 -0
- package/dist/build/index.d.ts +47 -0
- package/dist/build/index.d.ts.map +1 -0
- package/dist/build/index.js +113 -0
- package/dist/build/index.js.map +1 -0
- package/dist/client/index.d.ts +74 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +912 -0
- package/dist/client/index.js.map +1 -0
- package/dist/contract/index.d.ts +138 -0
- package/dist/contract/index.d.ts.map +1 -0
- package/dist/contract/index.js +33 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# @middag-io/licensing
|
|
2
|
+
|
|
3
|
+
Protected code delivery for MIDDAG products.
|
|
4
|
+
|
|
5
|
+
`@middag-io/licensing` standardizes how MIDDAG plugins (Moodle, WordPress,
|
|
6
|
+
and future hosts) ship licensed JavaScript modules through a CDN. The
|
|
7
|
+
package owns the technical contract (placeholders, manifest schema, types).
|
|
8
|
+
The licensing worker owns the policy (which install gets which module,
|
|
9
|
+
when, signed with what key).
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @middag-io/licensing
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Published to GitHub Packages (`@middag-io` scope). Configure `.npmrc` with
|
|
18
|
+
a token that has `read:packages` for installs and `write:packages` for
|
|
19
|
+
releases.
|
|
20
|
+
|
|
21
|
+
## Subpath exports
|
|
22
|
+
|
|
23
|
+
| Import | Purpose |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `@middag-io/licensing` | Shared contract types (`CONTRACT_VERSION`, `PLACEHOLDERS`, manifest interfaces). |
|
|
26
|
+
| `@middag-io/licensing/contract` | Same as root — explicit subpath. |
|
|
27
|
+
| `@middag-io/licensing/build` | Vite plugin and CLI helpers that inject placeholders and emit `manifest.template.json`. Node-only. |
|
|
28
|
+
| `@middag-io/licensing/client` | Runtime loader for the host. Validates the worker-issued manifest and dynamically imports protected modules. |
|
|
29
|
+
|
|
30
|
+
## Status
|
|
31
|
+
|
|
32
|
+
v0.0.2 — first implemented release. Tracks the protected-delivery worker
|
|
33
|
+
contract that shipped in `worker-ts-middag-licensing` v0.1.4 (ADR-012,
|
|
34
|
+
T1-T6). The Vite plugin emits `manifest.template.json` with SHA-384
|
|
35
|
+
hashes; the runtime loader verifies the worker-issued JWS against the
|
|
36
|
+
worker JWKS, validates the bootstrap (install / host / product /
|
|
37
|
+
contract version), and dynamic-imports modules with recomputed SRI.
|
|
38
|
+
|
|
39
|
+
Locked placeholder set (worker substitutes these per install):
|
|
40
|
+
|
|
41
|
+
- `__MIDDAG_HOST_ORIGIN__`
|
|
42
|
+
- `__MIDDAG_INSTALL_ID__`
|
|
43
|
+
- `__MIDDAG_KID__`
|
|
44
|
+
- `__MIDDAG_BUNDLE_EXPIRES_AT__`
|
|
45
|
+
- `__MIDDAG_NONCE__`
|
|
46
|
+
|
|
47
|
+
Adding, removing, or renaming a token is a coordinated worker change
|
|
48
|
+
(`src/lib/manifest-materialize.ts:PLACEHOLDERS`) and a `CONTRACT_VERSION`
|
|
49
|
+
bump.
|
|
50
|
+
|
|
51
|
+
Design contract:
|
|
52
|
+
[`worker-ts-middag-licensing/docs/explanation/spec-licensing-js-lib.md`](https://github.com/middag-io/worker-ts-middag-licensing/blob/main/docs/explanation/spec-licensing-js-lib.md).
|
|
53
|
+
|
|
54
|
+
## Development
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm install
|
|
58
|
+
npm run build
|
|
59
|
+
npm run typecheck
|
|
60
|
+
npm run test
|
|
61
|
+
npm run lint
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Release
|
|
65
|
+
|
|
66
|
+
1. `npm run changeset` to record the intent.
|
|
67
|
+
2. Merge to `main`. The Release workflow opens a "Version Packages" PR.
|
|
68
|
+
3. Merge that PR to publish.
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
Proprietary — MIDDAG.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time entry point for @middag-io/licensing.
|
|
3
|
+
*
|
|
4
|
+
* Exposes a Vite plugin that:
|
|
5
|
+
*
|
|
6
|
+
* 1. Scans each protected module's emitted artifact for the locked
|
|
7
|
+
* `__MIDDAG_*__` placeholder tokens.
|
|
8
|
+
* 2. Computes a SHA-384 hash of the placeholder-bearing template bytes
|
|
9
|
+
* (informational — the worker re-hashes post-substitution per install).
|
|
10
|
+
* 3. Emits `manifest.template.json` next to the bundle so product CI can
|
|
11
|
+
* upload it together with the artifacts to the worker's BUILDS bucket.
|
|
12
|
+
*
|
|
13
|
+
* The plugin does NOT substitute placeholders at build time — the developer
|
|
14
|
+
* writes the literal token (e.g. `__MIDDAG_INSTALL_ID__`) in their source,
|
|
15
|
+
* and the worker substitutes per-install at materialization. Build-time
|
|
16
|
+
* substitution would mean every install ships the same identifying bytes,
|
|
17
|
+
* defeating the per-install rotation that ADR-012 §"Protection model" relies
|
|
18
|
+
* on.
|
|
19
|
+
*/
|
|
20
|
+
import type { Plugin } from "vite";
|
|
21
|
+
import { CONTRACT_VERSION, PLACEHOLDERS, type ManifestTemplate, type ModuleDefinition } from "../contract/index.js";
|
|
22
|
+
export interface LicensingPluginOptions {
|
|
23
|
+
product: string;
|
|
24
|
+
release: string;
|
|
25
|
+
buildId: string;
|
|
26
|
+
modules: ModuleDefinition[];
|
|
27
|
+
/** Manifest filename (defaults to `manifest.template.json`). */
|
|
28
|
+
manifestFilename?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Behaviour when a protected module's emitted bytes contain none of the
|
|
31
|
+
* locked placeholders. Default `error` because shipping a "protected"
|
|
32
|
+
* module that has nothing to substitute is almost always a mistake (the
|
|
33
|
+
* developer forgot to insert the tokens, or a minifier renamed them).
|
|
34
|
+
* Set `warn` for libraries that only use a subset of placeholders.
|
|
35
|
+
*/
|
|
36
|
+
onMissingPlaceholders?: "error" | "warn";
|
|
37
|
+
}
|
|
38
|
+
export declare function licensingPlugin(options: LicensingPluginOptions): Plugin;
|
|
39
|
+
/** Convenience helper for tests / scripts that want the empty template. */
|
|
40
|
+
export declare function emptyManifestTemplate(opts: {
|
|
41
|
+
product: string;
|
|
42
|
+
release: string;
|
|
43
|
+
buildId: string;
|
|
44
|
+
}): ManifestTemplate;
|
|
45
|
+
export { CONTRACT_VERSION, PLACEHOLDERS };
|
|
46
|
+
export type { ManifestTemplate, ModuleDefinition };
|
|
47
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/build/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EAEtB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,gEAAgE;IAChE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CAC1C;AA6BD,wBAAgB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CA8EvE;AAmBD,2EAA2E;AAC3E,wBAAgB,qBAAqB,CAAC,IAAI,EAAE;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,gBAAgB,CAQnB;AAED,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,CAAC;AAC1C,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,CAAC"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { CONTRACT_VERSION, PLACEHOLDERS } from "../contract/index.js";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { posix } from "node:path";
|
|
4
|
+
//#region src/build/index.ts
|
|
5
|
+
/**
|
|
6
|
+
* Build-time entry point for @middag-io/licensing.
|
|
7
|
+
*
|
|
8
|
+
* Exposes a Vite plugin that:
|
|
9
|
+
*
|
|
10
|
+
* 1. Scans each protected module's emitted artifact for the locked
|
|
11
|
+
* `__MIDDAG_*__` placeholder tokens.
|
|
12
|
+
* 2. Computes a SHA-384 hash of the placeholder-bearing template bytes
|
|
13
|
+
* (informational — the worker re-hashes post-substitution per install).
|
|
14
|
+
* 3. Emits `manifest.template.json` next to the bundle so product CI can
|
|
15
|
+
* upload it together with the artifacts to the worker's BUILDS bucket.
|
|
16
|
+
*
|
|
17
|
+
* The plugin does NOT substitute placeholders at build time — the developer
|
|
18
|
+
* writes the literal token (e.g. `__MIDDAG_INSTALL_ID__`) in their source,
|
|
19
|
+
* and the worker substitutes per-install at materialization. Build-time
|
|
20
|
+
* substitution would mean every install ships the same identifying bytes,
|
|
21
|
+
* defeating the per-install rotation that ADR-012 §"Protection model" relies
|
|
22
|
+
* on.
|
|
23
|
+
*/
|
|
24
|
+
var PLACEHOLDER_ENTRIES = Object.entries(PLACEHOLDERS);
|
|
25
|
+
function sha384Sri(bytes) {
|
|
26
|
+
return `sha384-${createHash("sha384").update(bytes).digest("base64")}`;
|
|
27
|
+
}
|
|
28
|
+
function placeholdersSeen(source) {
|
|
29
|
+
const seen = [];
|
|
30
|
+
for (const [key, token] of PLACEHOLDER_ENTRIES) if (source.includes(token)) seen.push(key);
|
|
31
|
+
return seen;
|
|
32
|
+
}
|
|
33
|
+
function normalizeEntry(entry) {
|
|
34
|
+
return entry.split(/[\\/]+/).filter(Boolean).join("/");
|
|
35
|
+
}
|
|
36
|
+
function licensingPlugin(options) {
|
|
37
|
+
const manifestFilename = options.manifestFilename ?? "manifest.template.json";
|
|
38
|
+
const onMissing = options.onMissingPlaceholders ?? "error";
|
|
39
|
+
return {
|
|
40
|
+
name: "@middag-io/licensing",
|
|
41
|
+
apply: "build",
|
|
42
|
+
enforce: "post",
|
|
43
|
+
async generateBundle(_outputOptions, bundle) {
|
|
44
|
+
const emitted = /* @__PURE__ */ new Map();
|
|
45
|
+
for (const [filename, chunk] of Object.entries(bundle)) {
|
|
46
|
+
const norm = normalizeEntry(filename);
|
|
47
|
+
let bytes;
|
|
48
|
+
if (chunk.type === "chunk") bytes = new TextEncoder().encode(chunk.code);
|
|
49
|
+
else if (chunk.type === "asset") {
|
|
50
|
+
const source = chunk.source;
|
|
51
|
+
bytes = typeof source === "string" ? new TextEncoder().encode(source) : new Uint8Array(source);
|
|
52
|
+
}
|
|
53
|
+
if (bytes) emitted.set(norm, bytes);
|
|
54
|
+
}
|
|
55
|
+
const moduleEntries = [];
|
|
56
|
+
const missing = [];
|
|
57
|
+
for (const mod of options.modules) {
|
|
58
|
+
const key = normalizeEntry(mod.entry);
|
|
59
|
+
const bytes = emitted.get(key) ?? emitted.get(posix.basename(key)) ?? findByBasenameSuffix(emitted, key);
|
|
60
|
+
if (!bytes) this.error(`@middag-io/licensing: module "${mod.name}" declared entry "${mod.entry}" was not present in this build output`);
|
|
61
|
+
const seen = placeholdersSeen(new TextDecoder().decode(bytes));
|
|
62
|
+
if (seen.length === 0) missing.push(mod.name);
|
|
63
|
+
moduleEntries.push({
|
|
64
|
+
name: mod.name,
|
|
65
|
+
entry: mod.entry,
|
|
66
|
+
extension: mod.extension,
|
|
67
|
+
entitlements: mod.entitlements,
|
|
68
|
+
filename: key,
|
|
69
|
+
hash_sha384: sha384Sri(bytes),
|
|
70
|
+
placeholders_seen: seen
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (missing.length > 0) {
|
|
74
|
+
const msg = `@middag-io/licensing: protected module(s) had no placeholders: ${missing.join(", ")}. Ensure your source code uses tokens from PLACEHOLDERS (e.g. __MIDDAG_INSTALL_ID__) and that no plugin renames or strips them.`;
|
|
75
|
+
if (onMissing === "error") this.error(msg);
|
|
76
|
+
else this.warn(msg);
|
|
77
|
+
}
|
|
78
|
+
const template = {
|
|
79
|
+
contract_version: CONTRACT_VERSION,
|
|
80
|
+
product: options.product,
|
|
81
|
+
release: options.release,
|
|
82
|
+
build_id: options.buildId,
|
|
83
|
+
modules: moduleEntries
|
|
84
|
+
};
|
|
85
|
+
this.emitFile({
|
|
86
|
+
type: "asset",
|
|
87
|
+
fileName: manifestFilename,
|
|
88
|
+
source: JSON.stringify(template, null, 2) + "\n"
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function findByBasenameSuffix(emitted, declared) {
|
|
94
|
+
const base = posix.basename(declared).replace(/\.[^.]+$/, "");
|
|
95
|
+
for (const [filename, bytes] of emitted) {
|
|
96
|
+
const file = posix.basename(filename);
|
|
97
|
+
if (file.startsWith(base + "-") || file === posix.basename(declared)) return bytes;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Convenience helper for tests / scripts that want the empty template. */
|
|
101
|
+
function emptyManifestTemplate(opts) {
|
|
102
|
+
return {
|
|
103
|
+
contract_version: CONTRACT_VERSION,
|
|
104
|
+
product: opts.product,
|
|
105
|
+
release: opts.release,
|
|
106
|
+
build_id: opts.buildId,
|
|
107
|
+
modules: []
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
export { CONTRACT_VERSION, PLACEHOLDERS, emptyManifestTemplate, licensingPlugin };
|
|
112
|
+
|
|
113
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/build/index.ts"],"sourcesContent":["/**\n * Build-time entry point for @middag-io/licensing.\n *\n * Exposes a Vite plugin that:\n *\n * 1. Scans each protected module's emitted artifact for the locked\n * `__MIDDAG_*__` placeholder tokens.\n * 2. Computes a SHA-384 hash of the placeholder-bearing template bytes\n * (informational — the worker re-hashes post-substitution per install).\n * 3. Emits `manifest.template.json` next to the bundle so product CI can\n * upload it together with the artifacts to the worker's BUILDS bucket.\n *\n * The plugin does NOT substitute placeholders at build time — the developer\n * writes the literal token (e.g. `__MIDDAG_INSTALL_ID__`) in their source,\n * and the worker substitutes per-install at materialization. Build-time\n * substitution would mean every install ships the same identifying bytes,\n * defeating the per-install rotation that ADR-012 §\"Protection model\" relies\n * on.\n */\n\nimport { createHash } from \"node:crypto\";\nimport { posix } from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport {\n CONTRACT_VERSION,\n PLACEHOLDERS,\n type ManifestTemplate,\n type ModuleDefinition,\n type PlaceholderKey,\n} from \"../contract/index.js\";\n\nexport interface LicensingPluginOptions {\n product: string;\n release: string;\n buildId: string;\n modules: ModuleDefinition[];\n /** Manifest filename (defaults to `manifest.template.json`). */\n manifestFilename?: string;\n /**\n * Behaviour when a protected module's emitted bytes contain none of the\n * locked placeholders. Default `error` because shipping a \"protected\"\n * module that has nothing to substitute is almost always a mistake (the\n * developer forgot to insert the tokens, or a minifier renamed them).\n * Set `warn` for libraries that only use a subset of placeholders.\n */\n onMissingPlaceholders?: \"error\" | \"warn\";\n}\n\nconst PLACEHOLDER_ENTRIES = Object.entries(PLACEHOLDERS) as Array<\n [PlaceholderKey, (typeof PLACEHOLDERS)[PlaceholderKey]]\n>;\n\nfunction sha384Sri(bytes: Uint8Array): string {\n const digest = createHash(\"sha384\").update(bytes).digest(\"base64\");\n return `sha384-${digest}`;\n}\n\nfunction placeholdersSeen(source: string): PlaceholderKey[] {\n const seen: PlaceholderKey[] = [];\n for (const [key, token] of PLACEHOLDER_ENTRIES) {\n if (source.includes(token)) seen.push(key);\n }\n return seen;\n}\n\nfunction normalizeEntry(entry: string): string {\n // Vite's emit pipeline keys assets by their forward-slash path relative to\n // outDir. Convert any Windows-style separators so Vite-on-Windows matches\n // the same key Vite-on-macOS would produce.\n return entry\n .split(/[\\\\/]+/)\n .filter(Boolean)\n .join(\"/\");\n}\n\nexport function licensingPlugin(options: LicensingPluginOptions): Plugin {\n const manifestFilename = options.manifestFilename ?? \"manifest.template.json\";\n const onMissing = options.onMissingPlaceholders ?? \"error\";\n\n return {\n name: \"@middag-io/licensing\",\n apply: \"build\",\n enforce: \"post\",\n\n async generateBundle(_outputOptions, bundle) {\n const emitted = new Map<string, Uint8Array>();\n for (const [filename, chunk] of Object.entries(bundle)) {\n const norm = normalizeEntry(filename);\n let bytes: Uint8Array | undefined;\n if (chunk.type === \"chunk\") {\n bytes = new TextEncoder().encode(chunk.code);\n } else if (chunk.type === \"asset\") {\n const source = chunk.source;\n bytes =\n typeof source === \"string\" ? new TextEncoder().encode(source) : new Uint8Array(source);\n }\n if (bytes) emitted.set(norm, bytes);\n }\n\n const moduleEntries: ManifestTemplate[\"modules\"] = [];\n const missing: string[] = [];\n\n for (const mod of options.modules) {\n const key = normalizeEntry(mod.entry);\n const bytes =\n emitted.get(key) ??\n emitted.get(posix.basename(key)) ??\n findByBasenameSuffix(emitted, key);\n\n if (!bytes) {\n this.error(\n `@middag-io/licensing: module \"${mod.name}\" declared entry \"${mod.entry}\" was not present in this build output`,\n );\n }\n\n const source = new TextDecoder().decode(bytes);\n const seen = placeholdersSeen(source);\n if (seen.length === 0) {\n missing.push(mod.name);\n }\n\n moduleEntries.push({\n name: mod.name,\n entry: mod.entry,\n extension: mod.extension,\n entitlements: mod.entitlements,\n filename: key,\n hash_sha384: sha384Sri(bytes),\n placeholders_seen: seen,\n });\n }\n\n if (missing.length > 0) {\n const msg = `@middag-io/licensing: protected module(s) had no placeholders: ${missing.join(\", \")}. Ensure your source code uses tokens from PLACEHOLDERS (e.g. __MIDDAG_INSTALL_ID__) and that no plugin renames or strips them.`;\n if (onMissing === \"error\") this.error(msg);\n else this.warn(msg);\n }\n\n const template: ManifestTemplate = {\n contract_version: CONTRACT_VERSION,\n product: options.product,\n release: options.release,\n build_id: options.buildId,\n modules: moduleEntries,\n };\n\n this.emitFile({\n type: \"asset\",\n fileName: manifestFilename,\n source: JSON.stringify(template, null, 2) + \"\\n\",\n });\n },\n };\n}\n\n// Some Vite/Rollup configs hash entry filenames. If the declared `entry` is\n// `dist/core.js` but Vite emits `core-abc123.js`, fall back to matching by\n// basename stem so a user-friendly entry path still resolves.\nfunction findByBasenameSuffix(\n emitted: Map<string, Uint8Array>,\n declared: string,\n): Uint8Array | undefined {\n const base = posix.basename(declared).replace(/\\.[^.]+$/, \"\");\n for (const [filename, bytes] of emitted) {\n const file = posix.basename(filename);\n if (file.startsWith(base + \"-\") || file === posix.basename(declared)) {\n return bytes;\n }\n }\n return undefined;\n}\n\n/** Convenience helper for tests / scripts that want the empty template. */\nexport function emptyManifestTemplate(opts: {\n product: string;\n release: string;\n buildId: string;\n}): ManifestTemplate {\n return {\n contract_version: CONTRACT_VERSION,\n product: opts.product,\n release: opts.release,\n build_id: opts.buildId,\n modules: [],\n };\n}\n\nexport { CONTRACT_VERSION, PLACEHOLDERS };\nexport type { ManifestTemplate, ModuleDefinition };\n"],"mappings":"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"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime loader for @middag-io/licensing.
|
|
3
|
+
*
|
|
4
|
+
* Runs inside the consuming host (Moodle, WordPress, ...). The browser never
|
|
5
|
+
* calls the worker's HMAC-protected `/v1/protected/manifest` endpoint — the
|
|
6
|
+
* PHP host bridge does that, persists the `ManifestResponse`, and serves it
|
|
7
|
+
* to the browser at `manifest_url` (per ADR-012 §"Server-side host bridge").
|
|
8
|
+
*
|
|
9
|
+
* This loader:
|
|
10
|
+
*
|
|
11
|
+
* 1. Fetches the manifest JSON from the host-served URL (or via a caller-
|
|
12
|
+
* supplied `fetchManifest` for embedded use cases).
|
|
13
|
+
* 2. Verifies the JWS signature against the worker's JWKS endpoint
|
|
14
|
+
* `${workerOrigin}/api/protected/jwks` (cached in-memory per kid).
|
|
15
|
+
* 3. Validates the decoded payload against the bootstrap context
|
|
16
|
+
* (`install_id`, `host_origin`, `host_type`, `product`, `contract_version`)
|
|
17
|
+
* so a manifest swapped between installs is rejected client-side.
|
|
18
|
+
* 4. Loads a requested module by fetching its CDN URL, recomputing the
|
|
19
|
+
* SRI hash against the bytes, and dynamic-importing from a Blob URL.
|
|
20
|
+
* Native `import()` does not yet take an integrity attribute, so we
|
|
21
|
+
* compute SRI ourselves before handing the bytes to the JS engine.
|
|
22
|
+
*/
|
|
23
|
+
import { type JWK } from "jose";
|
|
24
|
+
import { CONTRACT_VERSION, type AuthorizedManifest, type HostType, type ManifestResponse } from "../contract/index.js";
|
|
25
|
+
export interface CreateLicensingClientOptions {
|
|
26
|
+
hostType: HostType;
|
|
27
|
+
hostOrigin: string;
|
|
28
|
+
product: string;
|
|
29
|
+
installId: string;
|
|
30
|
+
/**
|
|
31
|
+
* Worker base origin used to fetch the JWKS, e.g.
|
|
32
|
+
* `https://licensing.middag.io`. Required unless `jwks` is supplied.
|
|
33
|
+
*/
|
|
34
|
+
workerOrigin?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Static JWKS document (escape hatch for tests / pre-fetched keys).
|
|
37
|
+
* When supplied, the loader skips the network fetch.
|
|
38
|
+
*/
|
|
39
|
+
jwks?: {
|
|
40
|
+
keys: JWK[];
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Manifest URL served by the host (PHP bridge). The body must be the
|
|
44
|
+
* JSON shape worker returns from POST /v1/protected/manifest.
|
|
45
|
+
*/
|
|
46
|
+
manifestUrl?: string;
|
|
47
|
+
/** Caller-supplied manifest fetcher; overrides `manifestUrl` when given. */
|
|
48
|
+
fetchManifest?: () => Promise<ManifestResponse>;
|
|
49
|
+
/**
|
|
50
|
+
* Fetch implementation override. Defaults to `globalThis.fetch`. Tests
|
|
51
|
+
* can pass a mock here; SSR contexts can pass a polyfill.
|
|
52
|
+
*/
|
|
53
|
+
fetch?: typeof fetch;
|
|
54
|
+
/**
|
|
55
|
+
* Clock skew tolerance in seconds when checking `bundle_expires_at`.
|
|
56
|
+
* Defaults to 60s.
|
|
57
|
+
*/
|
|
58
|
+
clockSkewSeconds?: number;
|
|
59
|
+
}
|
|
60
|
+
export interface LicensingClient {
|
|
61
|
+
readonly manifest: AuthorizedManifest;
|
|
62
|
+
readonly response: ManifestResponse;
|
|
63
|
+
has(moduleName: string): boolean;
|
|
64
|
+
loadModule<T = unknown>(moduleName: string): Promise<T>;
|
|
65
|
+
refresh(): Promise<AuthorizedManifest>;
|
|
66
|
+
}
|
|
67
|
+
export declare class LicensingError extends Error {
|
|
68
|
+
readonly code: string;
|
|
69
|
+
constructor(code: string, message: string);
|
|
70
|
+
}
|
|
71
|
+
export declare function createLicensingClient(options: CreateLicensingClientOptions): Promise<LicensingClient>;
|
|
72
|
+
export { CONTRACT_VERSION };
|
|
73
|
+
export type { AuthorizedManifest, HostType, ManifestResponse };
|
|
74
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAA4B,KAAK,GAAG,EAAE,MAAM,MAAM,CAAC;AAC1D,OAAO,EACL,gBAAgB,EAChB,KAAK,kBAAkB,EACvB,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,QAAQ,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,GAAG,EAAE,CAAA;KAAE,CAAC;IACvB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAChD;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC;IACjC,UAAU,CAAC,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACxD,OAAO,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACxC;AA0DD,qBAAa,cAAe,SAAQ,KAAK;aAErB,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM,EAC5B,OAAO,EAAE,MAAM;CAKlB;AA2ID,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,4BAA4B,GACpC,OAAO,CAAC,eAAe,CAAC,CAmE1B;AAED,OAAO,EAAE,gBAAgB,EAAE,CAAC;AAC5B,YAAY,EAAE,kBAAkB,EAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC"}
|