@ansh-ui/license 0.2.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/dist/index.cjs +135 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +111 -0
- package/dist/index.d.ts +111 -0
- package/dist/index.js +126 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ansh contributors
|
|
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/dist/index.cjs
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@ansh-ui/core');
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
var KEY_PREFIX = "aur_";
|
|
7
|
+
var EDITION_CAPABILITIES = {
|
|
8
|
+
free: ["effect:core", "behaviour:core", "renderer:canvas2d"],
|
|
9
|
+
pro: [
|
|
10
|
+
"effect:core",
|
|
11
|
+
"effect:pro",
|
|
12
|
+
"behaviour:core",
|
|
13
|
+
"behaviour:pro",
|
|
14
|
+
"renderer:canvas2d",
|
|
15
|
+
"renderer:webgl"
|
|
16
|
+
],
|
|
17
|
+
enterprise: [
|
|
18
|
+
"effect:core",
|
|
19
|
+
"effect:pro",
|
|
20
|
+
"effect:enterprise",
|
|
21
|
+
"behaviour:core",
|
|
22
|
+
"behaviour:pro",
|
|
23
|
+
"renderer:canvas2d",
|
|
24
|
+
"renderer:webgl",
|
|
25
|
+
"renderer:webgpu",
|
|
26
|
+
"worker"
|
|
27
|
+
]
|
|
28
|
+
};
|
|
29
|
+
var activeEdition = "free";
|
|
30
|
+
var activePayload = null;
|
|
31
|
+
async function verifyLicense(key) {
|
|
32
|
+
const trimmed = key.trim();
|
|
33
|
+
if (!trimmed.startsWith(KEY_PREFIX)) {
|
|
34
|
+
return fail(`Key does not start with "${KEY_PREFIX}". Check for a copy-paste error.`);
|
|
35
|
+
}
|
|
36
|
+
const body = trimmed.slice(KEY_PREFIX.length);
|
|
37
|
+
const separator = body.lastIndexOf(".");
|
|
38
|
+
if (separator <= 0) {
|
|
39
|
+
return fail("Key is missing its signature separator. It may be truncated.");
|
|
40
|
+
}
|
|
41
|
+
const payloadPart = body.slice(0, separator);
|
|
42
|
+
body.slice(separator + 1);
|
|
43
|
+
let payload;
|
|
44
|
+
let payloadBytes;
|
|
45
|
+
try {
|
|
46
|
+
payloadBytes = base64UrlDecode(payloadPart);
|
|
47
|
+
payload = JSON.parse(new TextDecoder().decode(payloadBytes));
|
|
48
|
+
} catch {
|
|
49
|
+
return fail("Key payload is not valid base64url-encoded JSON.");
|
|
50
|
+
}
|
|
51
|
+
{
|
|
52
|
+
return {
|
|
53
|
+
valid: false,
|
|
54
|
+
payload,
|
|
55
|
+
edition: "free",
|
|
56
|
+
reason: "License verification is unconfigured in this build of @ansh-ui/license. This is not a problem with your key. See https://ansh.dev/docs/licensing"
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function activateLicense(key) {
|
|
61
|
+
const result = await verifyLicense(key);
|
|
62
|
+
if (!result.valid) {
|
|
63
|
+
console.warn(`[ansh] Licence not activated: ${result.reason}`);
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
activeEdition = result.edition;
|
|
67
|
+
activePayload = result.payload;
|
|
68
|
+
const registry = core.getRegistry();
|
|
69
|
+
for (const capability of EDITION_CAPABILITIES[result.edition]) {
|
|
70
|
+
registry.provide(capability);
|
|
71
|
+
}
|
|
72
|
+
for (const grant of result.payload?.grants ?? []) {
|
|
73
|
+
registry.provide(grant);
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
function useFreeEdition() {
|
|
78
|
+
activeEdition = "free";
|
|
79
|
+
activePayload = null;
|
|
80
|
+
const registry = core.getRegistry();
|
|
81
|
+
for (const capability of EDITION_CAPABILITIES.free) registry.provide(capability);
|
|
82
|
+
const paid = [
|
|
83
|
+
.../* @__PURE__ */ new Set([...EDITION_CAPABILITIES.pro, ...EDITION_CAPABILITIES.enterprise])
|
|
84
|
+
].filter((capability) => !EDITION_CAPABILITIES.free.includes(capability));
|
|
85
|
+
for (const capability of paid) {
|
|
86
|
+
registry.revoke(
|
|
87
|
+
capability,
|
|
88
|
+
"This component requires Ansh Pro. See https://ansh.dev/pro to compare editions."
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function currentEdition() {
|
|
93
|
+
return activeEdition;
|
|
94
|
+
}
|
|
95
|
+
function currentLicense() {
|
|
96
|
+
return activePayload;
|
|
97
|
+
}
|
|
98
|
+
function hasCapability(capability) {
|
|
99
|
+
return core.getRegistry().supports(capability);
|
|
100
|
+
}
|
|
101
|
+
function base64UrlDecode(input) {
|
|
102
|
+
const normalised = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
103
|
+
const padded = normalised + "=".repeat((4 - normalised.length % 4) % 4);
|
|
104
|
+
if (typeof atob === "function") {
|
|
105
|
+
const binary = atob(padded);
|
|
106
|
+
const bytes = new Uint8Array(binary.length);
|
|
107
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
108
|
+
return bytes;
|
|
109
|
+
}
|
|
110
|
+
return new Uint8Array(Buffer.from(padded, "base64"));
|
|
111
|
+
}
|
|
112
|
+
function fail(reason) {
|
|
113
|
+
return { valid: false, payload: null, edition: "free", reason };
|
|
114
|
+
}
|
|
115
|
+
function inspectLicense(key) {
|
|
116
|
+
try {
|
|
117
|
+
const body = key.trim().slice(KEY_PREFIX.length);
|
|
118
|
+
const separator = body.lastIndexOf(".");
|
|
119
|
+
if (separator <= 0) return null;
|
|
120
|
+
return JSON.parse(new TextDecoder().decode(base64UrlDecode(body.slice(0, separator))));
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
exports.EDITION_CAPABILITIES = EDITION_CAPABILITIES;
|
|
127
|
+
exports.activateLicense = activateLicense;
|
|
128
|
+
exports.currentEdition = currentEdition;
|
|
129
|
+
exports.currentLicense = currentLicense;
|
|
130
|
+
exports.hasCapability = hasCapability;
|
|
131
|
+
exports.inspectLicense = inspectLicense;
|
|
132
|
+
exports.useFreeEdition = useFreeEdition;
|
|
133
|
+
exports.verifyLicense = verifyLicense;
|
|
134
|
+
//# sourceMappingURL=index.cjs.map
|
|
135
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["getRegistry"],"mappings":"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","file":"index.cjs","sourcesContent":["import { getRegistry } from '@ansh-ui/core'\n\n/**\n * Edition gating and license key verification.\n *\n * ## What this is, honestly\n *\n * This is **not DRM, and it cannot be.** Any check that runs in a browser can be removed by editing\n * the bundle, and any check that runs at build time can be removed by patching the package. Nothing\n * shipped as JavaScript to a client is enforceable. Pretending otherwise would mean building\n * obfuscation and phone-home telemetry that hurts paying customers — slower builds, offline CI\n * failures, privacy concerns — while stopping nobody who was determined.\n *\n * So the design goal is different: **make it easy and obvious to be correctly licensed.** Concretely:\n *\n * - A signed key proves an entitlement without any network call, so CI works offline and no usage\n * data leaves the customer's machine.\n * - Pro components live behind a capability check that produces a clear, linkable error instead of\n * a mysteriously blank canvas.\n * - In development, an unlicensed Pro component renders a visible watermark and logs once. In\n * production it throws at mount. Neither is a puzzle to diagnose.\n * - Pro code lives in a separate entry point, so a Free-edition bundle never contains it. That is\n * a genuine benefit for Free users — smaller bundles — rather than a restriction.\n *\n * Enforcement, where it matters, happens at the point of sale and in the license agreement. This\n * module's job is to keep honest customers out of trouble.\n *\n * ## Key format\n *\n * `aur_<base64url(payload)>.<base64url(signature)>`\n *\n * The payload is JSON, so a customer can inspect exactly what they were issued — expiry, seats,\n * edition — without a tool. The signature is Ed25519 over the payload bytes.\n */\n\nexport type Edition = 'free' | 'pro' | 'enterprise'\n\nexport interface LicensePayload {\n /** Who the licence was issued to. Shown in diagnostics. */\n licensee: string\n edition: Edition\n /** ISO 8601 issue date. */\n issued: string\n /**\n * ISO 8601 expiry, or `null` for a perpetual licence.\n *\n * A perpetual licence for a version range is the honest model for a component library: the code a\n * customer already installed keeps working forever, and the licence governs access to *new*\n * versions. A licence that switches off working software is hostile.\n */\n expires: string | null\n /** Number of developer seats. Informational — not enforced at runtime. */\n seats?: number\n /** Capabilities this key grants beyond the edition's defaults. */\n grants?: string[]\n /** Domains the licence covers. Informational. */\n domains?: string[]\n}\n\nexport interface VerificationResult {\n valid: boolean\n payload: LicensePayload | null\n edition: Edition\n /** Human-readable explanation when `valid` is false. */\n reason?: string\n /** `true` when the signature verified but the licence has expired. */\n expired?: boolean\n}\n\n/**\n * The Ansh public signing key, raw Ed25519 (32 bytes), base64url.\n *\n * Public by definition — it can only verify, never sign. Shipping it in the package is what makes\n * offline verification possible.\n *\n * This placeholder is the all-zero key, which verifies nothing. It is replaced at release time by\n * the real key; until then `verifyLicense` reports `unconfigured` rather than silently accepting or\n * rejecting everything, so an integration bug can't be mistaken for a licensing one.\n */\nconst PUBLIC_KEY_B64 = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'\n\nconst KEY_PREFIX = 'aur_'\n\n/** Capabilities each edition grants. */\nconst EDITION_CAPABILITIES: Record<Edition, readonly string[]> = {\n free: ['effect:core', 'behaviour:core', 'renderer:canvas2d'],\n pro: [\n 'effect:core',\n 'effect:pro',\n 'behaviour:core',\n 'behaviour:pro',\n 'renderer:canvas2d',\n 'renderer:webgl',\n ],\n enterprise: [\n 'effect:core',\n 'effect:pro',\n 'effect:enterprise',\n 'behaviour:core',\n 'behaviour:pro',\n 'renderer:canvas2d',\n 'renderer:webgl',\n 'renderer:webgpu',\n 'worker',\n ],\n}\n\nlet activeEdition: Edition = 'free'\nlet activePayload: LicensePayload | null = null\n\n/**\n * Verify a licence key.\n *\n * Async because Web Crypto is async. Call it once at application startup (or in a build plugin) and\n * `await` it — the result is cached in module state, so components check a boolean rather than\n * re-verifying.\n *\n * Never throws. A malformed key, an unsupported runtime and a genuinely invalid signature all\n * produce a `VerificationResult` with a `reason`, because a licence problem must not be able to\n * crash an application.\n */\nexport async function verifyLicense(key: string): Promise<VerificationResult> {\n const trimmed = key.trim()\n\n if (!trimmed.startsWith(KEY_PREFIX)) {\n return fail(`Key does not start with \"${KEY_PREFIX}\". Check for a copy-paste error.`)\n }\n\n const body = trimmed.slice(KEY_PREFIX.length)\n const separator = body.lastIndexOf('.')\n if (separator <= 0) {\n return fail('Key is missing its signature separator. It may be truncated.')\n }\n\n const payloadPart = body.slice(0, separator)\n const signaturePart = body.slice(separator + 1)\n\n let payload: LicensePayload\n let payloadBytes: Uint8Array\n try {\n payloadBytes = base64UrlDecode(payloadPart)\n payload = JSON.parse(new TextDecoder().decode(payloadBytes)) as LicensePayload\n } catch {\n return fail('Key payload is not valid base64url-encoded JSON.')\n }\n\n if (PUBLIC_KEY_B64 === 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') {\n // Distinguish \"we haven't shipped a signing key yet\" from \"your key is bad\". Conflating the two\n // would send customers to support for a problem that is entirely ours.\n return {\n valid: false,\n payload,\n edition: 'free',\n reason:\n 'License verification is unconfigured in this build of @ansh-ui/license. ' +\n 'This is not a problem with your key. See https://ansh.dev/docs/licensing',\n }\n }\n\n let signatureValid: boolean\n try {\n signatureValid = await verifySignature(payloadBytes, base64UrlDecode(signaturePart))\n } catch (error) {\n return fail(\n `Signature verification is unavailable in this runtime ` +\n `(${error instanceof Error ? error.message : 'unknown error'}). ` +\n `Ed25519 via Web Crypto requires Node 18.4+, Chrome 137+, Firefox 129+ or Safari 17+. ` +\n `Verify at build time instead if you must support older runtimes.`,\n )\n }\n\n if (!signatureValid) {\n return fail('Signature does not match. The key has been modified or is not genuine.')\n }\n\n const expired = payload.expires !== null && new Date(payload.expires).getTime() < Date.now()\n if (expired) {\n return {\n valid: false,\n payload,\n edition: 'free',\n expired: true,\n reason:\n `Licence expired on ${payload.expires}. ` +\n `Previously-installed versions continue to work; renew to access newer releases.`,\n }\n }\n\n return { valid: true, payload, edition: payload.edition }\n}\n\n/**\n * Activate a licence, granting its capabilities in the registry.\n *\n * This is the single call an application makes. After it resolves, Pro components mount normally\n * and `registry.requireCapability('effect:pro')` succeeds.\n *\n * ```ts\n * import { activateLicense } from '@ansh-ui/license'\n * await activateLicense(process.env.ANSH_LICENSE_KEY!)\n * ```\n */\nexport async function activateLicense(key: string): Promise<VerificationResult> {\n const result = await verifyLicense(key)\n\n if (!result.valid) {\n console.warn(`[ansh] Licence not activated: ${result.reason}`)\n return result\n }\n\n activeEdition = result.edition\n activePayload = result.payload\n\n const registry = getRegistry()\n for (const capability of EDITION_CAPABILITIES[result.edition]) {\n registry.provide(capability)\n }\n for (const grant of result.payload?.grants ?? []) {\n registry.provide(grant)\n }\n\n return result\n}\n\n/**\n * Apply the Free edition explicitly.\n *\n * Called automatically the first time a capability is checked without activation, so Free works with\n * no setup at all. Exposed for tests and for resetting between app instances.\n */\nexport function useFreeEdition(): void {\n activeEdition = 'free'\n activePayload = null\n\n const registry = getRegistry()\n for (const capability of EDITION_CAPABILITIES.free) registry.provide(capability)\n\n // Revoke paid capabilities with a message that says what to do, rather than leaving the generic\n // \"missing capability\" error to speak for itself.\n const paid = [\n ...new Set([...EDITION_CAPABILITIES.pro, ...EDITION_CAPABILITIES.enterprise]),\n ].filter((capability) => !EDITION_CAPABILITIES.free.includes(capability))\n\n for (const capability of paid) {\n registry.revoke(\n capability,\n 'This component requires Ansh Pro. See https://ansh.dev/pro to compare editions.',\n )\n }\n}\n\n/** The currently active edition. */\nexport function currentEdition(): Edition {\n return activeEdition\n}\n\n/** The active licence payload, or `null` on Free. */\nexport function currentLicense(): LicensePayload | null {\n return activePayload\n}\n\n/** Is a capability available under the active edition? */\nexport function hasCapability(capability: string): boolean {\n return getRegistry().supports(capability)\n}\n\n// ---------------------------------------------------------------------------\n// Crypto\n// ---------------------------------------------------------------------------\n\nlet cachedKey: CryptoKey | null = null\n\nasync function verifySignature(payload: Uint8Array, signature: Uint8Array): Promise<boolean> {\n const subtle = globalThis.crypto?.subtle\n if (!subtle) throw new Error('Web Crypto is unavailable')\n\n if (!cachedKey) {\n cachedKey = await subtle.importKey(\n 'raw',\n base64UrlDecode(PUBLIC_KEY_B64) as unknown as ArrayBuffer,\n { name: 'Ed25519' },\n false,\n ['verify'],\n )\n }\n\n return subtle.verify(\n { name: 'Ed25519' },\n cachedKey,\n signature as unknown as ArrayBuffer,\n payload as unknown as ArrayBuffer,\n )\n}\n\n/**\n * base64url → bytes.\n *\n * Hand-rolled rather than using `atob` because base64url uses `-`/`_` instead of `+`/`/` and omits\n * padding, and because `atob` does not exist in every Node context we might run in.\n */\nfunction base64UrlDecode(input: string): Uint8Array {\n const normalised = input.replace(/-/g, '+').replace(/_/g, '/')\n const padded = normalised + '='.repeat((4 - (normalised.length % 4)) % 4)\n\n if (typeof atob === 'function') {\n const binary = atob(padded)\n const bytes = new Uint8Array(binary.length)\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)\n return bytes\n }\n\n // Node without a global `atob` (very old versions, or a stripped runtime).\n return new Uint8Array(Buffer.from(padded, 'base64'))\n}\n\nfunction fail(reason: string): VerificationResult {\n return { valid: false, payload: null, edition: 'free', reason }\n}\n\n/**\n * Decode a key's payload **without verifying it**.\n *\n * For diagnostics only — printing \"your licence expires on X\" in a CLI, for instance. Never gate on\n * this: the payload is unauthenticated and trivially forged.\n */\nexport function inspectLicense(key: string): LicensePayload | null {\n try {\n const body = key.trim().slice(KEY_PREFIX.length)\n const separator = body.lastIndexOf('.')\n if (separator <= 0) return null\n return JSON.parse(new TextDecoder().decode(base64UrlDecode(body.slice(0, separator))))\n } catch {\n return null\n }\n}\n\nexport { EDITION_CAPABILITIES }\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edition gating and license key verification.
|
|
3
|
+
*
|
|
4
|
+
* ## What this is, honestly
|
|
5
|
+
*
|
|
6
|
+
* This is **not DRM, and it cannot be.** Any check that runs in a browser can be removed by editing
|
|
7
|
+
* the bundle, and any check that runs at build time can be removed by patching the package. Nothing
|
|
8
|
+
* shipped as JavaScript to a client is enforceable. Pretending otherwise would mean building
|
|
9
|
+
* obfuscation and phone-home telemetry that hurts paying customers — slower builds, offline CI
|
|
10
|
+
* failures, privacy concerns — while stopping nobody who was determined.
|
|
11
|
+
*
|
|
12
|
+
* So the design goal is different: **make it easy and obvious to be correctly licensed.** Concretely:
|
|
13
|
+
*
|
|
14
|
+
* - A signed key proves an entitlement without any network call, so CI works offline and no usage
|
|
15
|
+
* data leaves the customer's machine.
|
|
16
|
+
* - Pro components live behind a capability check that produces a clear, linkable error instead of
|
|
17
|
+
* a mysteriously blank canvas.
|
|
18
|
+
* - In development, an unlicensed Pro component renders a visible watermark and logs once. In
|
|
19
|
+
* production it throws at mount. Neither is a puzzle to diagnose.
|
|
20
|
+
* - Pro code lives in a separate entry point, so a Free-edition bundle never contains it. That is
|
|
21
|
+
* a genuine benefit for Free users — smaller bundles — rather than a restriction.
|
|
22
|
+
*
|
|
23
|
+
* Enforcement, where it matters, happens at the point of sale and in the license agreement. This
|
|
24
|
+
* module's job is to keep honest customers out of trouble.
|
|
25
|
+
*
|
|
26
|
+
* ## Key format
|
|
27
|
+
*
|
|
28
|
+
* `aur_<base64url(payload)>.<base64url(signature)>`
|
|
29
|
+
*
|
|
30
|
+
* The payload is JSON, so a customer can inspect exactly what they were issued — expiry, seats,
|
|
31
|
+
* edition — without a tool. The signature is Ed25519 over the payload bytes.
|
|
32
|
+
*/
|
|
33
|
+
type Edition = 'free' | 'pro' | 'enterprise';
|
|
34
|
+
interface LicensePayload {
|
|
35
|
+
/** Who the licence was issued to. Shown in diagnostics. */
|
|
36
|
+
licensee: string;
|
|
37
|
+
edition: Edition;
|
|
38
|
+
/** ISO 8601 issue date. */
|
|
39
|
+
issued: string;
|
|
40
|
+
/**
|
|
41
|
+
* ISO 8601 expiry, or `null` for a perpetual licence.
|
|
42
|
+
*
|
|
43
|
+
* A perpetual licence for a version range is the honest model for a component library: the code a
|
|
44
|
+
* customer already installed keeps working forever, and the licence governs access to *new*
|
|
45
|
+
* versions. A licence that switches off working software is hostile.
|
|
46
|
+
*/
|
|
47
|
+
expires: string | null;
|
|
48
|
+
/** Number of developer seats. Informational — not enforced at runtime. */
|
|
49
|
+
seats?: number;
|
|
50
|
+
/** Capabilities this key grants beyond the edition's defaults. */
|
|
51
|
+
grants?: string[];
|
|
52
|
+
/** Domains the licence covers. Informational. */
|
|
53
|
+
domains?: string[];
|
|
54
|
+
}
|
|
55
|
+
interface VerificationResult {
|
|
56
|
+
valid: boolean;
|
|
57
|
+
payload: LicensePayload | null;
|
|
58
|
+
edition: Edition;
|
|
59
|
+
/** Human-readable explanation when `valid` is false. */
|
|
60
|
+
reason?: string;
|
|
61
|
+
/** `true` when the signature verified but the licence has expired. */
|
|
62
|
+
expired?: boolean;
|
|
63
|
+
}
|
|
64
|
+
/** Capabilities each edition grants. */
|
|
65
|
+
declare const EDITION_CAPABILITIES: Record<Edition, readonly string[]>;
|
|
66
|
+
/**
|
|
67
|
+
* Verify a licence key.
|
|
68
|
+
*
|
|
69
|
+
* Async because Web Crypto is async. Call it once at application startup (or in a build plugin) and
|
|
70
|
+
* `await` it — the result is cached in module state, so components check a boolean rather than
|
|
71
|
+
* re-verifying.
|
|
72
|
+
*
|
|
73
|
+
* Never throws. A malformed key, an unsupported runtime and a genuinely invalid signature all
|
|
74
|
+
* produce a `VerificationResult` with a `reason`, because a licence problem must not be able to
|
|
75
|
+
* crash an application.
|
|
76
|
+
*/
|
|
77
|
+
declare function verifyLicense(key: string): Promise<VerificationResult>;
|
|
78
|
+
/**
|
|
79
|
+
* Activate a licence, granting its capabilities in the registry.
|
|
80
|
+
*
|
|
81
|
+
* This is the single call an application makes. After it resolves, Pro components mount normally
|
|
82
|
+
* and `registry.requireCapability('effect:pro')` succeeds.
|
|
83
|
+
*
|
|
84
|
+
* ```ts
|
|
85
|
+
* import { activateLicense } from '@ansh-ui/license'
|
|
86
|
+
* await activateLicense(process.env.ANSH_LICENSE_KEY!)
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
declare function activateLicense(key: string): Promise<VerificationResult>;
|
|
90
|
+
/**
|
|
91
|
+
* Apply the Free edition explicitly.
|
|
92
|
+
*
|
|
93
|
+
* Called automatically the first time a capability is checked without activation, so Free works with
|
|
94
|
+
* no setup at all. Exposed for tests and for resetting between app instances.
|
|
95
|
+
*/
|
|
96
|
+
declare function useFreeEdition(): void;
|
|
97
|
+
/** The currently active edition. */
|
|
98
|
+
declare function currentEdition(): Edition;
|
|
99
|
+
/** The active licence payload, or `null` on Free. */
|
|
100
|
+
declare function currentLicense(): LicensePayload | null;
|
|
101
|
+
/** Is a capability available under the active edition? */
|
|
102
|
+
declare function hasCapability(capability: string): boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Decode a key's payload **without verifying it**.
|
|
105
|
+
*
|
|
106
|
+
* For diagnostics only — printing "your licence expires on X" in a CLI, for instance. Never gate on
|
|
107
|
+
* this: the payload is unauthenticated and trivially forged.
|
|
108
|
+
*/
|
|
109
|
+
declare function inspectLicense(key: string): LicensePayload | null;
|
|
110
|
+
|
|
111
|
+
export { EDITION_CAPABILITIES, type Edition, type LicensePayload, type VerificationResult, activateLicense, currentEdition, currentLicense, hasCapability, inspectLicense, useFreeEdition, verifyLicense };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edition gating and license key verification.
|
|
3
|
+
*
|
|
4
|
+
* ## What this is, honestly
|
|
5
|
+
*
|
|
6
|
+
* This is **not DRM, and it cannot be.** Any check that runs in a browser can be removed by editing
|
|
7
|
+
* the bundle, and any check that runs at build time can be removed by patching the package. Nothing
|
|
8
|
+
* shipped as JavaScript to a client is enforceable. Pretending otherwise would mean building
|
|
9
|
+
* obfuscation and phone-home telemetry that hurts paying customers — slower builds, offline CI
|
|
10
|
+
* failures, privacy concerns — while stopping nobody who was determined.
|
|
11
|
+
*
|
|
12
|
+
* So the design goal is different: **make it easy and obvious to be correctly licensed.** Concretely:
|
|
13
|
+
*
|
|
14
|
+
* - A signed key proves an entitlement without any network call, so CI works offline and no usage
|
|
15
|
+
* data leaves the customer's machine.
|
|
16
|
+
* - Pro components live behind a capability check that produces a clear, linkable error instead of
|
|
17
|
+
* a mysteriously blank canvas.
|
|
18
|
+
* - In development, an unlicensed Pro component renders a visible watermark and logs once. In
|
|
19
|
+
* production it throws at mount. Neither is a puzzle to diagnose.
|
|
20
|
+
* - Pro code lives in a separate entry point, so a Free-edition bundle never contains it. That is
|
|
21
|
+
* a genuine benefit for Free users — smaller bundles — rather than a restriction.
|
|
22
|
+
*
|
|
23
|
+
* Enforcement, where it matters, happens at the point of sale and in the license agreement. This
|
|
24
|
+
* module's job is to keep honest customers out of trouble.
|
|
25
|
+
*
|
|
26
|
+
* ## Key format
|
|
27
|
+
*
|
|
28
|
+
* `aur_<base64url(payload)>.<base64url(signature)>`
|
|
29
|
+
*
|
|
30
|
+
* The payload is JSON, so a customer can inspect exactly what they were issued — expiry, seats,
|
|
31
|
+
* edition — without a tool. The signature is Ed25519 over the payload bytes.
|
|
32
|
+
*/
|
|
33
|
+
type Edition = 'free' | 'pro' | 'enterprise';
|
|
34
|
+
interface LicensePayload {
|
|
35
|
+
/** Who the licence was issued to. Shown in diagnostics. */
|
|
36
|
+
licensee: string;
|
|
37
|
+
edition: Edition;
|
|
38
|
+
/** ISO 8601 issue date. */
|
|
39
|
+
issued: string;
|
|
40
|
+
/**
|
|
41
|
+
* ISO 8601 expiry, or `null` for a perpetual licence.
|
|
42
|
+
*
|
|
43
|
+
* A perpetual licence for a version range is the honest model for a component library: the code a
|
|
44
|
+
* customer already installed keeps working forever, and the licence governs access to *new*
|
|
45
|
+
* versions. A licence that switches off working software is hostile.
|
|
46
|
+
*/
|
|
47
|
+
expires: string | null;
|
|
48
|
+
/** Number of developer seats. Informational — not enforced at runtime. */
|
|
49
|
+
seats?: number;
|
|
50
|
+
/** Capabilities this key grants beyond the edition's defaults. */
|
|
51
|
+
grants?: string[];
|
|
52
|
+
/** Domains the licence covers. Informational. */
|
|
53
|
+
domains?: string[];
|
|
54
|
+
}
|
|
55
|
+
interface VerificationResult {
|
|
56
|
+
valid: boolean;
|
|
57
|
+
payload: LicensePayload | null;
|
|
58
|
+
edition: Edition;
|
|
59
|
+
/** Human-readable explanation when `valid` is false. */
|
|
60
|
+
reason?: string;
|
|
61
|
+
/** `true` when the signature verified but the licence has expired. */
|
|
62
|
+
expired?: boolean;
|
|
63
|
+
}
|
|
64
|
+
/** Capabilities each edition grants. */
|
|
65
|
+
declare const EDITION_CAPABILITIES: Record<Edition, readonly string[]>;
|
|
66
|
+
/**
|
|
67
|
+
* Verify a licence key.
|
|
68
|
+
*
|
|
69
|
+
* Async because Web Crypto is async. Call it once at application startup (or in a build plugin) and
|
|
70
|
+
* `await` it — the result is cached in module state, so components check a boolean rather than
|
|
71
|
+
* re-verifying.
|
|
72
|
+
*
|
|
73
|
+
* Never throws. A malformed key, an unsupported runtime and a genuinely invalid signature all
|
|
74
|
+
* produce a `VerificationResult` with a `reason`, because a licence problem must not be able to
|
|
75
|
+
* crash an application.
|
|
76
|
+
*/
|
|
77
|
+
declare function verifyLicense(key: string): Promise<VerificationResult>;
|
|
78
|
+
/**
|
|
79
|
+
* Activate a licence, granting its capabilities in the registry.
|
|
80
|
+
*
|
|
81
|
+
* This is the single call an application makes. After it resolves, Pro components mount normally
|
|
82
|
+
* and `registry.requireCapability('effect:pro')` succeeds.
|
|
83
|
+
*
|
|
84
|
+
* ```ts
|
|
85
|
+
* import { activateLicense } from '@ansh-ui/license'
|
|
86
|
+
* await activateLicense(process.env.ANSH_LICENSE_KEY!)
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
declare function activateLicense(key: string): Promise<VerificationResult>;
|
|
90
|
+
/**
|
|
91
|
+
* Apply the Free edition explicitly.
|
|
92
|
+
*
|
|
93
|
+
* Called automatically the first time a capability is checked without activation, so Free works with
|
|
94
|
+
* no setup at all. Exposed for tests and for resetting between app instances.
|
|
95
|
+
*/
|
|
96
|
+
declare function useFreeEdition(): void;
|
|
97
|
+
/** The currently active edition. */
|
|
98
|
+
declare function currentEdition(): Edition;
|
|
99
|
+
/** The active licence payload, or `null` on Free. */
|
|
100
|
+
declare function currentLicense(): LicensePayload | null;
|
|
101
|
+
/** Is a capability available under the active edition? */
|
|
102
|
+
declare function hasCapability(capability: string): boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Decode a key's payload **without verifying it**.
|
|
105
|
+
*
|
|
106
|
+
* For diagnostics only — printing "your licence expires on X" in a CLI, for instance. Never gate on
|
|
107
|
+
* this: the payload is unauthenticated and trivially forged.
|
|
108
|
+
*/
|
|
109
|
+
declare function inspectLicense(key: string): LicensePayload | null;
|
|
110
|
+
|
|
111
|
+
export { EDITION_CAPABILITIES, type Edition, type LicensePayload, type VerificationResult, activateLicense, currentEdition, currentLicense, hasCapability, inspectLicense, useFreeEdition, verifyLicense };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { getRegistry } from '@ansh-ui/core';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var KEY_PREFIX = "aur_";
|
|
5
|
+
var EDITION_CAPABILITIES = {
|
|
6
|
+
free: ["effect:core", "behaviour:core", "renderer:canvas2d"],
|
|
7
|
+
pro: [
|
|
8
|
+
"effect:core",
|
|
9
|
+
"effect:pro",
|
|
10
|
+
"behaviour:core",
|
|
11
|
+
"behaviour:pro",
|
|
12
|
+
"renderer:canvas2d",
|
|
13
|
+
"renderer:webgl"
|
|
14
|
+
],
|
|
15
|
+
enterprise: [
|
|
16
|
+
"effect:core",
|
|
17
|
+
"effect:pro",
|
|
18
|
+
"effect:enterprise",
|
|
19
|
+
"behaviour:core",
|
|
20
|
+
"behaviour:pro",
|
|
21
|
+
"renderer:canvas2d",
|
|
22
|
+
"renderer:webgl",
|
|
23
|
+
"renderer:webgpu",
|
|
24
|
+
"worker"
|
|
25
|
+
]
|
|
26
|
+
};
|
|
27
|
+
var activeEdition = "free";
|
|
28
|
+
var activePayload = null;
|
|
29
|
+
async function verifyLicense(key) {
|
|
30
|
+
const trimmed = key.trim();
|
|
31
|
+
if (!trimmed.startsWith(KEY_PREFIX)) {
|
|
32
|
+
return fail(`Key does not start with "${KEY_PREFIX}". Check for a copy-paste error.`);
|
|
33
|
+
}
|
|
34
|
+
const body = trimmed.slice(KEY_PREFIX.length);
|
|
35
|
+
const separator = body.lastIndexOf(".");
|
|
36
|
+
if (separator <= 0) {
|
|
37
|
+
return fail("Key is missing its signature separator. It may be truncated.");
|
|
38
|
+
}
|
|
39
|
+
const payloadPart = body.slice(0, separator);
|
|
40
|
+
body.slice(separator + 1);
|
|
41
|
+
let payload;
|
|
42
|
+
let payloadBytes;
|
|
43
|
+
try {
|
|
44
|
+
payloadBytes = base64UrlDecode(payloadPart);
|
|
45
|
+
payload = JSON.parse(new TextDecoder().decode(payloadBytes));
|
|
46
|
+
} catch {
|
|
47
|
+
return fail("Key payload is not valid base64url-encoded JSON.");
|
|
48
|
+
}
|
|
49
|
+
{
|
|
50
|
+
return {
|
|
51
|
+
valid: false,
|
|
52
|
+
payload,
|
|
53
|
+
edition: "free",
|
|
54
|
+
reason: "License verification is unconfigured in this build of @ansh-ui/license. This is not a problem with your key. See https://ansh.dev/docs/licensing"
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function activateLicense(key) {
|
|
59
|
+
const result = await verifyLicense(key);
|
|
60
|
+
if (!result.valid) {
|
|
61
|
+
console.warn(`[ansh] Licence not activated: ${result.reason}`);
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
activeEdition = result.edition;
|
|
65
|
+
activePayload = result.payload;
|
|
66
|
+
const registry = getRegistry();
|
|
67
|
+
for (const capability of EDITION_CAPABILITIES[result.edition]) {
|
|
68
|
+
registry.provide(capability);
|
|
69
|
+
}
|
|
70
|
+
for (const grant of result.payload?.grants ?? []) {
|
|
71
|
+
registry.provide(grant);
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
function useFreeEdition() {
|
|
76
|
+
activeEdition = "free";
|
|
77
|
+
activePayload = null;
|
|
78
|
+
const registry = getRegistry();
|
|
79
|
+
for (const capability of EDITION_CAPABILITIES.free) registry.provide(capability);
|
|
80
|
+
const paid = [
|
|
81
|
+
.../* @__PURE__ */ new Set([...EDITION_CAPABILITIES.pro, ...EDITION_CAPABILITIES.enterprise])
|
|
82
|
+
].filter((capability) => !EDITION_CAPABILITIES.free.includes(capability));
|
|
83
|
+
for (const capability of paid) {
|
|
84
|
+
registry.revoke(
|
|
85
|
+
capability,
|
|
86
|
+
"This component requires Ansh Pro. See https://ansh.dev/pro to compare editions."
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function currentEdition() {
|
|
91
|
+
return activeEdition;
|
|
92
|
+
}
|
|
93
|
+
function currentLicense() {
|
|
94
|
+
return activePayload;
|
|
95
|
+
}
|
|
96
|
+
function hasCapability(capability) {
|
|
97
|
+
return getRegistry().supports(capability);
|
|
98
|
+
}
|
|
99
|
+
function base64UrlDecode(input) {
|
|
100
|
+
const normalised = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
101
|
+
const padded = normalised + "=".repeat((4 - normalised.length % 4) % 4);
|
|
102
|
+
if (typeof atob === "function") {
|
|
103
|
+
const binary = atob(padded);
|
|
104
|
+
const bytes = new Uint8Array(binary.length);
|
|
105
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
106
|
+
return bytes;
|
|
107
|
+
}
|
|
108
|
+
return new Uint8Array(Buffer.from(padded, "base64"));
|
|
109
|
+
}
|
|
110
|
+
function fail(reason) {
|
|
111
|
+
return { valid: false, payload: null, edition: "free", reason };
|
|
112
|
+
}
|
|
113
|
+
function inspectLicense(key) {
|
|
114
|
+
try {
|
|
115
|
+
const body = key.trim().slice(KEY_PREFIX.length);
|
|
116
|
+
const separator = body.lastIndexOf(".");
|
|
117
|
+
if (separator <= 0) return null;
|
|
118
|
+
return JSON.parse(new TextDecoder().decode(base64UrlDecode(body.slice(0, separator))));
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export { EDITION_CAPABILITIES, activateLicense, currentEdition, currentLicense, hasCapability, inspectLicense, useFreeEdition, verifyLicense };
|
|
125
|
+
//# sourceMappingURL=index.js.map
|
|
126
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { getRegistry } from '@ansh-ui/core'\n\n/**\n * Edition gating and license key verification.\n *\n * ## What this is, honestly\n *\n * This is **not DRM, and it cannot be.** Any check that runs in a browser can be removed by editing\n * the bundle, and any check that runs at build time can be removed by patching the package. Nothing\n * shipped as JavaScript to a client is enforceable. Pretending otherwise would mean building\n * obfuscation and phone-home telemetry that hurts paying customers — slower builds, offline CI\n * failures, privacy concerns — while stopping nobody who was determined.\n *\n * So the design goal is different: **make it easy and obvious to be correctly licensed.** Concretely:\n *\n * - A signed key proves an entitlement without any network call, so CI works offline and no usage\n * data leaves the customer's machine.\n * - Pro components live behind a capability check that produces a clear, linkable error instead of\n * a mysteriously blank canvas.\n * - In development, an unlicensed Pro component renders a visible watermark and logs once. In\n * production it throws at mount. Neither is a puzzle to diagnose.\n * - Pro code lives in a separate entry point, so a Free-edition bundle never contains it. That is\n * a genuine benefit for Free users — smaller bundles — rather than a restriction.\n *\n * Enforcement, where it matters, happens at the point of sale and in the license agreement. This\n * module's job is to keep honest customers out of trouble.\n *\n * ## Key format\n *\n * `aur_<base64url(payload)>.<base64url(signature)>`\n *\n * The payload is JSON, so a customer can inspect exactly what they were issued — expiry, seats,\n * edition — without a tool. The signature is Ed25519 over the payload bytes.\n */\n\nexport type Edition = 'free' | 'pro' | 'enterprise'\n\nexport interface LicensePayload {\n /** Who the licence was issued to. Shown in diagnostics. */\n licensee: string\n edition: Edition\n /** ISO 8601 issue date. */\n issued: string\n /**\n * ISO 8601 expiry, or `null` for a perpetual licence.\n *\n * A perpetual licence for a version range is the honest model for a component library: the code a\n * customer already installed keeps working forever, and the licence governs access to *new*\n * versions. A licence that switches off working software is hostile.\n */\n expires: string | null\n /** Number of developer seats. Informational — not enforced at runtime. */\n seats?: number\n /** Capabilities this key grants beyond the edition's defaults. */\n grants?: string[]\n /** Domains the licence covers. Informational. */\n domains?: string[]\n}\n\nexport interface VerificationResult {\n valid: boolean\n payload: LicensePayload | null\n edition: Edition\n /** Human-readable explanation when `valid` is false. */\n reason?: string\n /** `true` when the signature verified but the licence has expired. */\n expired?: boolean\n}\n\n/**\n * The Ansh public signing key, raw Ed25519 (32 bytes), base64url.\n *\n * Public by definition — it can only verify, never sign. Shipping it in the package is what makes\n * offline verification possible.\n *\n * This placeholder is the all-zero key, which verifies nothing. It is replaced at release time by\n * the real key; until then `verifyLicense` reports `unconfigured` rather than silently accepting or\n * rejecting everything, so an integration bug can't be mistaken for a licensing one.\n */\nconst PUBLIC_KEY_B64 = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'\n\nconst KEY_PREFIX = 'aur_'\n\n/** Capabilities each edition grants. */\nconst EDITION_CAPABILITIES: Record<Edition, readonly string[]> = {\n free: ['effect:core', 'behaviour:core', 'renderer:canvas2d'],\n pro: [\n 'effect:core',\n 'effect:pro',\n 'behaviour:core',\n 'behaviour:pro',\n 'renderer:canvas2d',\n 'renderer:webgl',\n ],\n enterprise: [\n 'effect:core',\n 'effect:pro',\n 'effect:enterprise',\n 'behaviour:core',\n 'behaviour:pro',\n 'renderer:canvas2d',\n 'renderer:webgl',\n 'renderer:webgpu',\n 'worker',\n ],\n}\n\nlet activeEdition: Edition = 'free'\nlet activePayload: LicensePayload | null = null\n\n/**\n * Verify a licence key.\n *\n * Async because Web Crypto is async. Call it once at application startup (or in a build plugin) and\n * `await` it — the result is cached in module state, so components check a boolean rather than\n * re-verifying.\n *\n * Never throws. A malformed key, an unsupported runtime and a genuinely invalid signature all\n * produce a `VerificationResult` with a `reason`, because a licence problem must not be able to\n * crash an application.\n */\nexport async function verifyLicense(key: string): Promise<VerificationResult> {\n const trimmed = key.trim()\n\n if (!trimmed.startsWith(KEY_PREFIX)) {\n return fail(`Key does not start with \"${KEY_PREFIX}\". Check for a copy-paste error.`)\n }\n\n const body = trimmed.slice(KEY_PREFIX.length)\n const separator = body.lastIndexOf('.')\n if (separator <= 0) {\n return fail('Key is missing its signature separator. It may be truncated.')\n }\n\n const payloadPart = body.slice(0, separator)\n const signaturePart = body.slice(separator + 1)\n\n let payload: LicensePayload\n let payloadBytes: Uint8Array\n try {\n payloadBytes = base64UrlDecode(payloadPart)\n payload = JSON.parse(new TextDecoder().decode(payloadBytes)) as LicensePayload\n } catch {\n return fail('Key payload is not valid base64url-encoded JSON.')\n }\n\n if (PUBLIC_KEY_B64 === 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') {\n // Distinguish \"we haven't shipped a signing key yet\" from \"your key is bad\". Conflating the two\n // would send customers to support for a problem that is entirely ours.\n return {\n valid: false,\n payload,\n edition: 'free',\n reason:\n 'License verification is unconfigured in this build of @ansh-ui/license. ' +\n 'This is not a problem with your key. See https://ansh.dev/docs/licensing',\n }\n }\n\n let signatureValid: boolean\n try {\n signatureValid = await verifySignature(payloadBytes, base64UrlDecode(signaturePart))\n } catch (error) {\n return fail(\n `Signature verification is unavailable in this runtime ` +\n `(${error instanceof Error ? error.message : 'unknown error'}). ` +\n `Ed25519 via Web Crypto requires Node 18.4+, Chrome 137+, Firefox 129+ or Safari 17+. ` +\n `Verify at build time instead if you must support older runtimes.`,\n )\n }\n\n if (!signatureValid) {\n return fail('Signature does not match. The key has been modified or is not genuine.')\n }\n\n const expired = payload.expires !== null && new Date(payload.expires).getTime() < Date.now()\n if (expired) {\n return {\n valid: false,\n payload,\n edition: 'free',\n expired: true,\n reason:\n `Licence expired on ${payload.expires}. ` +\n `Previously-installed versions continue to work; renew to access newer releases.`,\n }\n }\n\n return { valid: true, payload, edition: payload.edition }\n}\n\n/**\n * Activate a licence, granting its capabilities in the registry.\n *\n * This is the single call an application makes. After it resolves, Pro components mount normally\n * and `registry.requireCapability('effect:pro')` succeeds.\n *\n * ```ts\n * import { activateLicense } from '@ansh-ui/license'\n * await activateLicense(process.env.ANSH_LICENSE_KEY!)\n * ```\n */\nexport async function activateLicense(key: string): Promise<VerificationResult> {\n const result = await verifyLicense(key)\n\n if (!result.valid) {\n console.warn(`[ansh] Licence not activated: ${result.reason}`)\n return result\n }\n\n activeEdition = result.edition\n activePayload = result.payload\n\n const registry = getRegistry()\n for (const capability of EDITION_CAPABILITIES[result.edition]) {\n registry.provide(capability)\n }\n for (const grant of result.payload?.grants ?? []) {\n registry.provide(grant)\n }\n\n return result\n}\n\n/**\n * Apply the Free edition explicitly.\n *\n * Called automatically the first time a capability is checked without activation, so Free works with\n * no setup at all. Exposed for tests and for resetting between app instances.\n */\nexport function useFreeEdition(): void {\n activeEdition = 'free'\n activePayload = null\n\n const registry = getRegistry()\n for (const capability of EDITION_CAPABILITIES.free) registry.provide(capability)\n\n // Revoke paid capabilities with a message that says what to do, rather than leaving the generic\n // \"missing capability\" error to speak for itself.\n const paid = [\n ...new Set([...EDITION_CAPABILITIES.pro, ...EDITION_CAPABILITIES.enterprise]),\n ].filter((capability) => !EDITION_CAPABILITIES.free.includes(capability))\n\n for (const capability of paid) {\n registry.revoke(\n capability,\n 'This component requires Ansh Pro. See https://ansh.dev/pro to compare editions.',\n )\n }\n}\n\n/** The currently active edition. */\nexport function currentEdition(): Edition {\n return activeEdition\n}\n\n/** The active licence payload, or `null` on Free. */\nexport function currentLicense(): LicensePayload | null {\n return activePayload\n}\n\n/** Is a capability available under the active edition? */\nexport function hasCapability(capability: string): boolean {\n return getRegistry().supports(capability)\n}\n\n// ---------------------------------------------------------------------------\n// Crypto\n// ---------------------------------------------------------------------------\n\nlet cachedKey: CryptoKey | null = null\n\nasync function verifySignature(payload: Uint8Array, signature: Uint8Array): Promise<boolean> {\n const subtle = globalThis.crypto?.subtle\n if (!subtle) throw new Error('Web Crypto is unavailable')\n\n if (!cachedKey) {\n cachedKey = await subtle.importKey(\n 'raw',\n base64UrlDecode(PUBLIC_KEY_B64) as unknown as ArrayBuffer,\n { name: 'Ed25519' },\n false,\n ['verify'],\n )\n }\n\n return subtle.verify(\n { name: 'Ed25519' },\n cachedKey,\n signature as unknown as ArrayBuffer,\n payload as unknown as ArrayBuffer,\n )\n}\n\n/**\n * base64url → bytes.\n *\n * Hand-rolled rather than using `atob` because base64url uses `-`/`_` instead of `+`/`/` and omits\n * padding, and because `atob` does not exist in every Node context we might run in.\n */\nfunction base64UrlDecode(input: string): Uint8Array {\n const normalised = input.replace(/-/g, '+').replace(/_/g, '/')\n const padded = normalised + '='.repeat((4 - (normalised.length % 4)) % 4)\n\n if (typeof atob === 'function') {\n const binary = atob(padded)\n const bytes = new Uint8Array(binary.length)\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)\n return bytes\n }\n\n // Node without a global `atob` (very old versions, or a stripped runtime).\n return new Uint8Array(Buffer.from(padded, 'base64'))\n}\n\nfunction fail(reason: string): VerificationResult {\n return { valid: false, payload: null, edition: 'free', reason }\n}\n\n/**\n * Decode a key's payload **without verifying it**.\n *\n * For diagnostics only — printing \"your licence expires on X\" in a CLI, for instance. Never gate on\n * this: the payload is unauthenticated and trivially forged.\n */\nexport function inspectLicense(key: string): LicensePayload | null {\n try {\n const body = key.trim().slice(KEY_PREFIX.length)\n const separator = body.lastIndexOf('.')\n if (separator <= 0) return null\n return JSON.parse(new TextDecoder().decode(base64UrlDecode(body.slice(0, separator))))\n } catch {\n return null\n }\n}\n\nexport { EDITION_CAPABILITIES }\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ansh-ui/license",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Ansh edition gating and license key verification.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/jithin003/ui-library.git",
|
|
11
|
+
"directory": "packages/license"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"main": "./dist/index.cjs",
|
|
17
|
+
"module": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js",
|
|
23
|
+
"require": "./dist/index.cjs"
|
|
24
|
+
},
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@ansh-ui/core": "0.2.0"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsup",
|
|
35
|
+
"dev": "tsup --watch",
|
|
36
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
37
|
+
}
|
|
38
|
+
}
|