@zanii/certify 0.0.0-stage → 0.5.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 +159 -0
- package/README.md +112 -2
- package/dist/appliance-execution.d.ts +18 -0
- package/dist/appliance-execution.js +12 -0
- package/dist/appliance.d.ts +135 -0
- package/dist/appliance.js +253 -0
- package/dist/assurance-status.d.ts +58 -0
- package/dist/assurance-status.js +86 -0
- package/dist/audit-renewal.d.ts +39 -0
- package/dist/audit-renewal.js +62 -0
- package/dist/audit.d.ts +69 -0
- package/dist/audit.js +112 -0
- package/dist/beacon-client.d.ts +38 -0
- package/dist/beacon-client.js +52 -0
- package/dist/beacon.d.ts +84 -0
- package/dist/beacon.js +106 -0
- package/dist/bitstring-status.d.ts +56 -0
- package/dist/bitstring-status.js +135 -0
- package/dist/board.d.ts +218 -0
- package/dist/board.js +428 -0
- package/dist/bundle.d.ts +92 -0
- package/dist/bundle.js +109 -0
- package/dist/certificate.d.ts +161 -0
- package/dist/certificate.js +345 -0
- package/dist/cli-messages.json +148 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +369 -0
- package/dist/current-issuer-vc.d.ts +20 -0
- package/dist/current-issuer-vc.js +60 -0
- package/dist/evidence-share.d.ts +89 -0
- package/dist/evidence-share.js +479 -0
- package/dist/federation.d.ts +103 -0
- package/dist/federation.js +240 -0
- package/dist/feed-snapshot.d.ts +97 -0
- package/dist/feed-snapshot.js +186 -0
- package/dist/fingerprint.d.ts +23 -0
- package/dist/fingerprint.js +48 -0
- package/dist/formats/status-v2.schema.json +31 -0
- package/dist/governance.d.ts +242 -0
- package/dist/governance.js +411 -0
- package/dist/identity.d.ts +79 -0
- package/dist/identity.js +146 -0
- package/dist/impartiality.d.ts +47 -0
- package/dist/impartiality.js +85 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +59 -0
- package/dist/issuer-authorization.d.ts +4 -0
- package/dist/issuer-authorization.js +52 -0
- package/dist/issuer-resolution.d.ts +25 -0
- package/dist/issuer-resolution.js +89 -0
- package/dist/licence-owner-entry.d.ts +20 -0
- package/dist/licence-owner-entry.js +33 -0
- package/dist/licence-owner.d.ts +67 -0
- package/dist/licence-owner.js +180 -0
- package/dist/licence.d.ts +174 -0
- package/dist/licence.js +400 -0
- package/dist/live-evidence.d.ts +135 -0
- package/dist/live-evidence.js +210 -0
- package/dist/nitro-roots.d.ts +3 -0
- package/dist/nitro-roots.js +20 -0
- package/dist/nitro-x509.d.ts +16 -0
- package/dist/nitro-x509.js +141 -0
- package/dist/nitro.d.ts +74 -0
- package/dist/nitro.js +329 -0
- package/dist/normalization.d.ts +8 -0
- package/dist/normalization.js +10 -0
- package/dist/pq.d.ts +32 -0
- package/dist/pq.js +69 -0
- package/dist/profile.d.ts +20 -0
- package/dist/profile.js +161 -0
- package/dist/register.d.ts +79 -0
- package/dist/register.js +134 -0
- package/dist/rotation.d.ts +49 -0
- package/dist/rotation.js +124 -0
- package/dist/royalty-reconciliation.d.ts +24 -0
- package/dist/royalty-reconciliation.js +88 -0
- package/dist/runtime-client.d.ts +38 -0
- package/dist/runtime-client.js +185 -0
- package/dist/runtime-gateway.d.ts +24 -0
- package/dist/runtime-gateway.js +167 -0
- package/dist/runtime-knowledge.d.ts +28 -0
- package/dist/runtime-knowledge.js +75 -0
- package/dist/runtime-nitro.d.ts +30 -0
- package/dist/runtime-nitro.js +57 -0
- package/dist/runtime.d.ts +226 -0
- package/dist/runtime.js +254 -0
- package/dist/sandbox-delegation.d.ts +38 -0
- package/dist/sandbox-delegation.js +61 -0
- package/dist/schema-dialect.d.ts +1 -0
- package/dist/schema-dialect.js +97 -0
- package/dist/schemas/clause-map.schema.json +52 -0
- package/dist/schemas/profile.schema.json +80 -0
- package/dist/schemas/scenarios.schema.json +573 -0
- package/dist/schemas/suite.schema.json +138 -0
- package/dist/scheme-authority.d.ts +5 -0
- package/dist/scheme-authority.js +15 -0
- package/dist/scheme-entry.d.ts +25 -0
- package/dist/scheme-entry.js +85 -0
- package/dist/scheme-history.d.ts +5 -0
- package/dist/scheme-history.js +87 -0
- package/dist/scheme-log.d.ts +98 -0
- package/dist/scheme-log.js +266 -0
- package/dist/scheme-merkle.d.ts +2 -0
- package/dist/scheme-merkle.js +20 -0
- package/dist/scheme-text.d.ts +25 -0
- package/dist/scheme-text.js +46 -0
- package/dist/scheme-timing.d.ts +16 -0
- package/dist/scheme-timing.js +22 -0
- package/dist/scheme-transition.d.ts +47 -0
- package/dist/scheme-transition.js +101 -0
- package/dist/scoring.d.ts +35 -0
- package/dist/scoring.js +50 -0
- package/dist/shared.d.ts +16 -0
- package/dist/shared.js +20 -0
- package/dist/signer.d.ts +18 -0
- package/dist/signer.js +49 -0
- package/dist/signing-identity.d.ts +1 -0
- package/dist/signing-identity.js +11 -0
- package/dist/status.d.ts +113 -0
- package/dist/status.js +204 -0
- package/dist/succession.d.ts +21 -0
- package/dist/succession.js +67 -0
- package/dist/suite.d.ts +257 -0
- package/dist/suite.js +441 -0
- package/dist/vc.d.ts +29 -0
- package/dist/vc.js +96 -0
- package/dist/verification-bounds.d.ts +5 -0
- package/dist/verification-bounds.js +76 -0
- package/dist/verification-policy.d.ts +32 -0
- package/dist/verification-policy.js +85 -0
- package/dist/witness.d.ts +19 -0
- package/dist/witness.js +80 -0
- package/package.json +57 -3
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { publicKeyFromDid } from "@zanii/core";
|
|
2
|
+
import { isObj } from "./shared.js";
|
|
3
|
+
const failure = (error) => {
|
|
4
|
+
const e = error;
|
|
5
|
+
const code = e?.cause?.code ?? e?.code ?? "";
|
|
6
|
+
return { ok: false, code: e?.name === "TimeoutError" || e?.name === "AbortError" || code === "ETIMEDOUT" || code.endsWith("_TIMEOUT") ? "timeout"
|
|
7
|
+
: ["ENOTFOUND", "EAI_AGAIN", "ENODATA"].includes(code) ? "dns" : /^(?:ERR_TLS|ERR_SSL|CERT_|DEPTH_|SELF_SIGNED|UNABLE_TO_VERIFY|UNABLE_TO_GET_ISSUER_CERT)/.test(code) ? "tls" : "network" };
|
|
8
|
+
};
|
|
9
|
+
/** A single deadline covers headers and body, including injected transports that ignore AbortSignal. */
|
|
10
|
+
export async function fetchJsonBounded(url, opts = {}) {
|
|
11
|
+
const timeout = opts.timeoutMs ?? 10_000, limit = opts.maxBytes ?? 1_048_576;
|
|
12
|
+
if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > 10_000 || !Number.isSafeInteger(limit) || limit < 1 || limit > 8_388_608)
|
|
13
|
+
return { ok: false, code: "invalid_input" };
|
|
14
|
+
try {
|
|
15
|
+
const u = new URL(url);
|
|
16
|
+
if (!["http:", "https:"].includes(u.protocol) || u.username || u.password || u.hash)
|
|
17
|
+
return { ok: false, code: "invalid_input" };
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return { ok: false, code: "invalid_input" };
|
|
21
|
+
}
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
let timer;
|
|
24
|
+
const task = async () => {
|
|
25
|
+
const response = await (opts.fetch ?? fetch)(url, { redirect: "error", signal: controller.signal });
|
|
26
|
+
if (response.status !== 200) {
|
|
27
|
+
await response.body?.cancel();
|
|
28
|
+
return { ok: false, code: "http", status: response.status };
|
|
29
|
+
}
|
|
30
|
+
if (Number(response.headers.get("content-length")) > limit) {
|
|
31
|
+
await response.body?.cancel();
|
|
32
|
+
return { ok: false, code: "too_large" };
|
|
33
|
+
}
|
|
34
|
+
const reader = response.body?.getReader();
|
|
35
|
+
const chunks = [];
|
|
36
|
+
let size = 0;
|
|
37
|
+
if (reader)
|
|
38
|
+
try {
|
|
39
|
+
for (;;) {
|
|
40
|
+
const read = await reader.read();
|
|
41
|
+
if (read.done)
|
|
42
|
+
break;
|
|
43
|
+
size += read.value.length;
|
|
44
|
+
if (size > limit) {
|
|
45
|
+
void reader.cancel().catch(() => { });
|
|
46
|
+
return { ok: false, code: "too_large" };
|
|
47
|
+
}
|
|
48
|
+
chunks.push(read.value);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
reader.releaseLock();
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
return { ok: true, document: JSON.parse(new TextDecoder("utf8", { fatal: true }).decode(Buffer.concat(chunks))) };
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return { ok: false, code: "document" };
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
try {
|
|
62
|
+
return await Promise.race([task(), new Promise((resolve) => { timer = setTimeout(() => { controller.abort(); resolve({ ok: false, code: "timeout" }); }, timeout); })]);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
return failure(error);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
if (timer)
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
controller.abort();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Unlike the legacy null resolver, distinguishes a failed fetch from a document that omits the key. */
|
|
74
|
+
export async function resolveIssuerDetailed(did, domain, opts = {}) {
|
|
75
|
+
if (typeof did !== "string" || did.length > 69 || !publicKeyFromDid(did) || typeof domain !== "string" || domain.length > 253
|
|
76
|
+
|| !/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/.test(domain.toLowerCase()) || domain.trim() !== domain)
|
|
77
|
+
return { ok: false, code: "invalid_input" };
|
|
78
|
+
const host = domain.toLowerCase();
|
|
79
|
+
const result = await fetchJsonBounded(`https://${host}/.well-known/zanii-issuer.json`, opts);
|
|
80
|
+
if (!result.ok)
|
|
81
|
+
return result;
|
|
82
|
+
const doc = result.document;
|
|
83
|
+
if (!isObj(doc) || doc.v !== 1 || typeof doc.domain !== "string" || doc.domain.toLowerCase() !== host || typeof doc.name !== "string"
|
|
84
|
+
|| doc.name.length > 200 || !Array.isArray(doc.dids) || doc.dids.length > 64 || !doc.dids.every((d) => typeof d === "string" && d.length <= 69 && publicKeyFromDid(d)))
|
|
85
|
+
return { ok: false, code: "document" };
|
|
86
|
+
if (!doc.dids.includes(did))
|
|
87
|
+
return { ok: false, code: "key_missing" };
|
|
88
|
+
return { ok: true, issuer: { did, name: doc.name, domain: host }, document: doc };
|
|
89
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type LicenceState } from "./licence.ts";
|
|
2
|
+
export interface LicenceOwnerEntryContext {
|
|
3
|
+
root: string;
|
|
4
|
+
currentOwner: string;
|
|
5
|
+
size: number;
|
|
6
|
+
hash: string | null;
|
|
7
|
+
at: string | null;
|
|
8
|
+
prefixRoot: string;
|
|
9
|
+
/** Lookups expose only the already authenticated prefix. */
|
|
10
|
+
knownOwner: (did: string) => boolean;
|
|
11
|
+
licenceState: (hash: string) => LicenceState;
|
|
12
|
+
}
|
|
13
|
+
export interface LicenceOwnerEntryCheck {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
reasons: string[];
|
|
16
|
+
currentOwner: string | null;
|
|
17
|
+
at?: string;
|
|
18
|
+
licenceState?: LicenceState;
|
|
19
|
+
}
|
|
20
|
+
export declare function verifyLicenceOwnerEntry(doc: unknown, c: LicenceOwnerEntryContext): LicenceOwnerEntryCheck;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// One extension of a trusted mixed owner-history prefix; the caller commits metadata atomically.
|
|
2
|
+
import { publicKeyFromDid, verifySignedObject, verifySTH } from "@zanii/core";
|
|
3
|
+
import { signersOf } from "./board.js";
|
|
4
|
+
import { licenceStatusFieldError } from "./licence.js";
|
|
5
|
+
import { isLicenceOwnerCheckpoint, isLicenceOwnerRotation } from "./licence-owner.js";
|
|
6
|
+
import { isInt, isIso, isObj, isSha, ms } from "./shared.js";
|
|
7
|
+
import { withinVerificationBudget } from "./verification-bounds.js";
|
|
8
|
+
const did = (v) => typeof v === "string" && v.length <= 69 && publicKeyFromDid(v) !== null;
|
|
9
|
+
export function verifyLicenceOwnerEntry(doc, c) {
|
|
10
|
+
const denied = (reason) => ({ ok: false, reasons: [reason], currentOwner: null });
|
|
11
|
+
if (!withinVerificationBudget(doc, { maxBytes: 1_048_576, maxNodes: 100_000 }) || !withinVerificationBudget({ root: c?.root, currentOwner: c?.currentOwner, size: c?.size, hash: c?.hash, at: c?.at, prefixRoot: c?.prefixRoot }, { maxBytes: 4096, maxNodes: 100 })
|
|
12
|
+
|| !did(c?.root) || !did(c?.currentOwner) || !isInt(c?.size, 0, 9999) || !(c.size === 0 ? c.hash === null && c.at === null && c.currentOwner === c.root : isSha(c.hash) && isIso(c.at)) || !isSha(c?.prefixRoot) || typeof c?.knownOwner !== "function" || typeof c?.licenceState !== "function")
|
|
13
|
+
return denied("a bounded authenticated licence-owner prefix is required");
|
|
14
|
+
if (!isObj(doc) || doc.seq !== c.size || doc.prev !== c.hash || !isIso(doc.at) || (c.at !== null && ms(doc.at) < ms(c.at)))
|
|
15
|
+
return denied("owner history entry has an invalid position or time");
|
|
16
|
+
if (doc.type === "certify.licence-owner-rotation") {
|
|
17
|
+
if (!isLicenceOwnerRotation(doc) || doc.from !== c.currentOwner || c.knownOwner(doc.to) || (c.at !== null && ms(doc.at) <= ms(c.at)))
|
|
18
|
+
return denied("owner rotation has an invalid signer, cutover or reused key");
|
|
19
|
+
const cp = doc.checkpoint;
|
|
20
|
+
if (!isLicenceOwnerCheckpoint(cp) || cp.log_id !== c.currentOwner || cp.size !== c.size || cp.root !== c.prefixRoot || !verifySTH(cp) || ms(cp.ts) > ms(doc.at) || (c.at !== null && ms(cp.ts) < ms(c.at)))
|
|
21
|
+
return denied("owner rotation does not bind the preceding history checkpoint");
|
|
22
|
+
const signed = signersOf(doc, [c.currentOwner, doc.to]);
|
|
23
|
+
if (!signed.wellFormed || signed.signers.length !== 2)
|
|
24
|
+
return denied("owner rotation needs the retiring and successor signatures");
|
|
25
|
+
return { ok: true, reasons: [], currentOwner: doc.to, at: doc.at };
|
|
26
|
+
}
|
|
27
|
+
if (doc.v !== 1 || doc.type !== "certify.licence-status" || Object.keys(doc).length !== ("for_cause" in doc || "effective_at" in doc ? 12 : 10) || doc.signer !== c.currentOwner || licenceStatusFieldError(doc) || !verifySignedObject(doc))
|
|
28
|
+
return denied("owner history entry is not a status entry signed by the current owner");
|
|
29
|
+
const was = c.licenceState(doc.licence);
|
|
30
|
+
if (!["active", "suspended", "withdrawn"].includes(was) || (doc.action === "suspend" && was !== "active") || (doc.action === "reinstate" && was !== "suspended") || (doc.action === "withdraw" && was === "withdrawn"))
|
|
31
|
+
return denied("owner history entry has an invalid licence state transition");
|
|
32
|
+
return { ok: true, reasons: [], currentOwner: c.currentOwner, at: doc.at, licenceState: doc.action === "suspend" ? "suspended" : doc.action === "reinstate" ? "active" : "withdrawn" };
|
|
33
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type SignedTreeHead } from "@zanii/core";
|
|
2
|
+
import { type BodySignature } from "./board.ts";
|
|
3
|
+
import { type Licence } from "./licence.ts";
|
|
4
|
+
import { type Signer } from "./signer.ts";
|
|
5
|
+
export interface LicenceOwnerRotation {
|
|
6
|
+
v: 1;
|
|
7
|
+
type: "certify.licence-owner-rotation";
|
|
8
|
+
seq: number;
|
|
9
|
+
prev: string | null;
|
|
10
|
+
from: string;
|
|
11
|
+
to: string;
|
|
12
|
+
at: string;
|
|
13
|
+
checkpoint: SignedTreeHead;
|
|
14
|
+
licences: {
|
|
15
|
+
size: number;
|
|
16
|
+
root: string;
|
|
17
|
+
};
|
|
18
|
+
signatures: BodySignature[];
|
|
19
|
+
}
|
|
20
|
+
export interface OwnerLicenceProof {
|
|
21
|
+
rotation: string;
|
|
22
|
+
licence: string;
|
|
23
|
+
index: number;
|
|
24
|
+
size: number;
|
|
25
|
+
path: string[];
|
|
26
|
+
}
|
|
27
|
+
export interface LicenceOwnerHistoryPolicy {
|
|
28
|
+
log: readonly unknown[];
|
|
29
|
+
checkpoint: SignedTreeHead;
|
|
30
|
+
licenceProofs: readonly OwnerLicenceProof[];
|
|
31
|
+
}
|
|
32
|
+
export interface LicenceOwnerHistoryCheck {
|
|
33
|
+
ok: boolean;
|
|
34
|
+
reasons: string[];
|
|
35
|
+
currentOwner: string | null;
|
|
36
|
+
owners: {
|
|
37
|
+
did: string;
|
|
38
|
+
from: string | null;
|
|
39
|
+
}[];
|
|
40
|
+
rotations: LicenceOwnerRotation[];
|
|
41
|
+
}
|
|
42
|
+
declare const checkpointShape: (v: unknown) => v is SignedTreeHead;
|
|
43
|
+
declare const rotationShape: (v: unknown) => v is LicenceOwnerRotation;
|
|
44
|
+
export declare const isLicenceOwnerRotation: typeof rotationShape;
|
|
45
|
+
export declare const isLicenceOwnerCheckpoint: typeof checkpointShape;
|
|
46
|
+
export declare function verifyLicenceOwnerHistory(log: readonly unknown[], opts: {
|
|
47
|
+
root: string;
|
|
48
|
+
checkpoint: SignedTreeHead;
|
|
49
|
+
}): LicenceOwnerHistoryCheck;
|
|
50
|
+
export declare function licenceOwnerCheckpoint(log: readonly unknown[], at: string, signer: Signer, opts: {
|
|
51
|
+
root: string;
|
|
52
|
+
}): Promise<SignedTreeHead>;
|
|
53
|
+
export declare function buildLicenceOwnerRotation(input: {
|
|
54
|
+
history: readonly unknown[];
|
|
55
|
+
root: string;
|
|
56
|
+
licences: readonly Licence[];
|
|
57
|
+
at: string;
|
|
58
|
+
}, retiring: Signer, successor: Signer): Promise<LicenceOwnerRotation>;
|
|
59
|
+
export declare function licenceOwnerProofs(rotation: LicenceOwnerRotation, licences: readonly Licence[], hashes: readonly string[]): OwnerLicenceProof[];
|
|
60
|
+
export declare const licenceOwnerProof: (rotation: LicenceOwnerRotation, licences: readonly Licence[], hash: string) => OwnerLicenceProof;
|
|
61
|
+
export declare function verifyLicenceWithOwnerHistory(licence: unknown, opts: LicenceOwnerHistoryPolicy & {
|
|
62
|
+
root: string;
|
|
63
|
+
}): {
|
|
64
|
+
ok: boolean;
|
|
65
|
+
reasons: string[];
|
|
66
|
+
};
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Versioned owner succession keeps status state and commits retired-owner issuance before cutover.
|
|
2
|
+
import { canonicalBytes, createSTHAsync, jcsHash, leafHash, merkleRoot, publicKeyFromDid, verifyInclusion, verifySTH } from "@zanii/core";
|
|
3
|
+
import { bodySignature, signersOf, withSignatures } from "./board.js";
|
|
4
|
+
import { licenceStatusFieldError, verifyLicence } from "./licence.js";
|
|
5
|
+
import { isInt, isIso, isObj, isSha, ms } from "./shared.js";
|
|
6
|
+
import { signatureOk, signerDid } from "./signer.js";
|
|
7
|
+
import { withinVerificationBudget } from "./verification-bounds.js";
|
|
8
|
+
const did = (v) => typeof v === "string" && v.length <= 69 && publicKeyFromDid(v) !== null;
|
|
9
|
+
const digest = (v) => isSha(v) && v.length === 71;
|
|
10
|
+
const signature = (v) => typeof v === "string" && v.length === 136 && /^ed25519:[0-9a-f]{128}$/.test(v);
|
|
11
|
+
const checkpointShape = (v) => isObj(v) && Object.keys(v).length === 6 && v.v === 1 && did(v.log_id) && digest(v.root) && isInt(v.size, 0, 10_000) && isIso(v.ts) && signature(v.sig);
|
|
12
|
+
const rotationShape = (v) => isObj(v) && Object.keys(v).length === 10 && v.v === 1 && v.type === "certify.licence-owner-rotation"
|
|
13
|
+
&& isInt(v.seq, 0, 10_000) && (v.seq === 0 ? v.prev === null : digest(v.prev)) && did(v.from) && did(v.to) && v.from !== v.to && isIso(v.at)
|
|
14
|
+
&& checkpointShape(v.checkpoint) && isObj(v.licences) && Object.keys(v.licences).length === 2 && isInt(v.licences.size, 0, 50_000) && digest(v.licences.root)
|
|
15
|
+
&& Array.isArray(v.signatures) && v.signatures.length === 2 && Array.from(v.signatures).every((s) => isObj(s) && Object.keys(s).length === 2 && did(s.signer) && signature(s.sig));
|
|
16
|
+
const proofShape = (v) => isObj(v) && Object.keys(v).length === 5 && digest(v.rotation) && digest(v.licence) && isInt(v.size, 1, 50_000) && isInt(v.index, 0, v.size - 1)
|
|
17
|
+
&& Array.isArray(v.path) && v.path.length <= 32 && Array.from(v.path).every((p) => typeof p === "string" && p.length === 64 && /^[0-9a-f]{64}$/.test(p));
|
|
18
|
+
export const isLicenceOwnerRotation = rotationShape;
|
|
19
|
+
export const isLicenceOwnerCheckpoint = checkpointShape;
|
|
20
|
+
const hashHex = (b) => `sha256:${Buffer.from(b).toString("hex")}`;
|
|
21
|
+
function frontier() {
|
|
22
|
+
const levels = [];
|
|
23
|
+
let size = 0;
|
|
24
|
+
return {
|
|
25
|
+
append(value) {
|
|
26
|
+
let node = leafHash(canonicalBytes(value)), n = size++, level = 0;
|
|
27
|
+
while (n % 2) {
|
|
28
|
+
node = merkleRoot([levels[level], node]);
|
|
29
|
+
levels[level] = undefined;
|
|
30
|
+
n = Math.floor(n / 2);
|
|
31
|
+
level++;
|
|
32
|
+
}
|
|
33
|
+
levels[level] = node;
|
|
34
|
+
},
|
|
35
|
+
root() {
|
|
36
|
+
let root;
|
|
37
|
+
for (const node of levels)
|
|
38
|
+
if (node)
|
|
39
|
+
root = root ? merkleRoot([node, root]) : node;
|
|
40
|
+
return hashHex(root ?? merkleRoot([]));
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function historyCheck(log, root, checkpoint, building = false) {
|
|
45
|
+
const denied = (reason) => ({ ok: false, reasons: [reason], currentOwner: null, owners: [], rotations: [] });
|
|
46
|
+
if (!did(root) || (!building && !checkpointShape(checkpoint)))
|
|
47
|
+
return denied("trusted owner root and independently pinned checkpoint are required");
|
|
48
|
+
if (!Array.isArray(log) || log.length > 10_000 || (!building && !log.length) || !withinVerificationBudget({ log, root, ...(checkpoint ? { checkpoint } : {}) }))
|
|
49
|
+
return denied("owner history exceeds bounds or is empty");
|
|
50
|
+
let current = root, previous = null;
|
|
51
|
+
const owners = [{ did: root, from: null }], rotations = [], state = new Map(), tree = frontier();
|
|
52
|
+
for (let i = 0; i < log.length; i++) {
|
|
53
|
+
const row = log[i];
|
|
54
|
+
if (!isObj(row) || row.seq !== i || row.prev !== (i ? jcsHash(previous) : null) || !isIso(row.at) || (previous && ms(row.at) < ms(previous.at)))
|
|
55
|
+
return denied(`owner history entry ${i} has an invalid position or time`);
|
|
56
|
+
if (row.type === "certify.licence-owner-rotation") {
|
|
57
|
+
if (!rotationShape(row) || row.from !== current || owners.some((o) => o.did === row.to) || (previous && ms(row.at) <= ms(previous.at)))
|
|
58
|
+
return denied(`owner rotation ${i} has an invalid signer, cutover or reused key`);
|
|
59
|
+
const cp = row.checkpoint;
|
|
60
|
+
if (cp.log_id !== current || cp.size !== i || cp.root !== tree.root() || !verifySTH(cp) || ms(cp.ts) > ms(row.at) || (previous && ms(cp.ts) < ms(previous.at)))
|
|
61
|
+
return denied(`owner rotation ${i} does not bind the preceding history checkpoint`);
|
|
62
|
+
const signed = signersOf(row, [current, row.to]);
|
|
63
|
+
if (!signed.wellFormed || signed.signers.length !== 2)
|
|
64
|
+
return denied(`owner rotation ${i} needs the retiring and successor signatures`);
|
|
65
|
+
current = row.to;
|
|
66
|
+
owners.push({ did: current, from: row.at });
|
|
67
|
+
rotations.push(row);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
if (row.v !== 1 || row.type !== "certify.licence-status" || Object.keys(row).length !== ("for_cause" in row || "effective_at" in row ? 12 : 10)
|
|
71
|
+
|| row.signer !== current || !signature(row.sig) || !signatureOk(Object.fromEntries(Object.entries(row).filter(([key]) => key !== "sig")), row.sig, current)
|
|
72
|
+
|| licenceStatusFieldError(row) || !digest(row.licence))
|
|
73
|
+
return denied(`owner history entry ${i} is not a status entry signed by the current owner`);
|
|
74
|
+
const was = state.get(row.licence) ?? "active";
|
|
75
|
+
const action = row.action;
|
|
76
|
+
if ((action === "suspend" && was !== "active") || (action === "reinstate" && was !== "suspended") || (action === "withdraw" && was === "withdrawn"))
|
|
77
|
+
return denied(`owner history entry ${i} has an invalid licence state transition`);
|
|
78
|
+
state.set(row.licence, action === "suspend" ? "suspended" : action === "reinstate" ? "active" : "withdrawn");
|
|
79
|
+
}
|
|
80
|
+
tree.append(row);
|
|
81
|
+
previous = row;
|
|
82
|
+
}
|
|
83
|
+
if (!building && (!checkpoint || checkpoint.log_id !== current || checkpoint.size !== log.length || checkpoint.root !== tree.root() || !verifySTH(checkpoint) || (previous && ms(checkpoint.ts) < ms(previous.at))))
|
|
84
|
+
return denied("pinned owner checkpoint does not cover this complete history");
|
|
85
|
+
return { ok: true, reasons: [], currentOwner: current, owners, rotations };
|
|
86
|
+
}
|
|
87
|
+
export function verifyLicenceOwnerHistory(log, opts) {
|
|
88
|
+
return historyCheck(log, opts?.root, opts?.checkpoint);
|
|
89
|
+
}
|
|
90
|
+
export async function licenceOwnerCheckpoint(log, at, signer, opts) {
|
|
91
|
+
const checked = historyCheck(log, opts?.root, undefined, true), last = log?.at(-1);
|
|
92
|
+
if (!checked.ok || checked.currentOwner !== signerDid(signer) || !isIso(at) || (last && ms(at) < ms(last.at)))
|
|
93
|
+
throw new Error("invalid owner checkpoint signer, history or time");
|
|
94
|
+
const tree = frontier();
|
|
95
|
+
for (const row of log)
|
|
96
|
+
tree.append(row);
|
|
97
|
+
return createSTHAsync({ logId: signerDid(signer), size: log.length, root: tree.root(), ts: at }, signer.sign);
|
|
98
|
+
}
|
|
99
|
+
const licenceLeaf = (hash) => leafHash(canonicalBytes({ v: 1, type: "certify.owner-licence-leaf", licence: hash }));
|
|
100
|
+
function licenceSnapshot(licences, owner, at) {
|
|
101
|
+
if (!Array.isArray(licences) || licences.length > 50_000 || !withinVerificationBudget(licences))
|
|
102
|
+
throw new Error("owner issuance snapshot exceeds bounds");
|
|
103
|
+
const hashes = licences.map((licence) => {
|
|
104
|
+
if (!verifyLicence(licence, { owner }).ok || ms(licence.issued_at) > ms(at))
|
|
105
|
+
throw new Error("invalid retiring-owner licence snapshot");
|
|
106
|
+
return jcsHash(licence);
|
|
107
|
+
}).sort();
|
|
108
|
+
if (new Set(hashes).size !== hashes.length)
|
|
109
|
+
throw new Error("duplicate retiring-owner licence");
|
|
110
|
+
const levels = [hashes.map(licenceLeaf)];
|
|
111
|
+
while (levels.at(-1).length > 1) {
|
|
112
|
+
const level = levels.at(-1);
|
|
113
|
+
const next = [];
|
|
114
|
+
for (let i = 0; i < level.length; i += 2)
|
|
115
|
+
next.push(i + 1 < level.length ? merkleRoot([level[i], level[i + 1]]) : level[i]);
|
|
116
|
+
levels.push(next);
|
|
117
|
+
}
|
|
118
|
+
return { hashes, levels, root: hashHex(levels.at(-1)[0] ?? merkleRoot([])) };
|
|
119
|
+
}
|
|
120
|
+
export async function buildLicenceOwnerRotation(input, retiring, successor) {
|
|
121
|
+
if (!withinVerificationBudget(input) || !isIso(input.at) || !Array.isArray(input.history) || input.history.length >= 10_000)
|
|
122
|
+
throw new Error("invalid owner rotation input");
|
|
123
|
+
const checked = historyCheck(input.history, input.root, undefined, true), from = signerDid(retiring), to = signerDid(successor);
|
|
124
|
+
const prior = input.history.at(-1);
|
|
125
|
+
if (!checked.ok || checked.currentOwner !== from || checked.owners.some((o) => o.did === to) || (prior && ms(input.at) <= ms(prior.at)))
|
|
126
|
+
throw new Error("invalid retiring/successor owner or cutover history");
|
|
127
|
+
const issued = licenceSnapshot(input.licences, from, input.at), tree = frontier();
|
|
128
|
+
for (const entry of input.history)
|
|
129
|
+
tree.append(entry);
|
|
130
|
+
const seq = input.history.length, prev = seq ? jcsHash(prior) : null, at = input.at;
|
|
131
|
+
const cp = await createSTHAsync({ logId: from, size: seq, root: tree.root(), ts: at }, retiring.sign);
|
|
132
|
+
const body = { v: 1, type: "certify.licence-owner-rotation", seq, prev, from, to, at, checkpoint: cp, licences: { size: issued.hashes.length, root: issued.root } };
|
|
133
|
+
return withSignatures(body, await Promise.all([bodySignature(body, retiring), bodySignature(body, successor)]));
|
|
134
|
+
}
|
|
135
|
+
export function licenceOwnerProofs(rotation, licences, hashes) {
|
|
136
|
+
if (!rotationShape(rotation) || !Array.isArray(hashes) || hashes.length < 1 || hashes.length > 100 || !Array.from(hashes).every(digest) || !withinVerificationBudget({ rotation, licences, hashes }))
|
|
137
|
+
throw new Error("invalid owner licence proof request");
|
|
138
|
+
const snapshot = licenceSnapshot(licences, rotation.from, rotation.at);
|
|
139
|
+
if (snapshot.root !== rotation.licences.root || snapshot.hashes.length !== rotation.licences.size)
|
|
140
|
+
throw new Error("licences do not match the committed cutover snapshot");
|
|
141
|
+
const positions = new Map(snapshot.hashes.map((hash, i) => [hash, i]));
|
|
142
|
+
return hashes.map((hash) => {
|
|
143
|
+
const index = positions.get(hash);
|
|
144
|
+
if (index === undefined)
|
|
145
|
+
throw new Error("licence is not in the cutover snapshot");
|
|
146
|
+
const path = [];
|
|
147
|
+
let at = index;
|
|
148
|
+
for (const level of snapshot.levels.slice(0, -1)) {
|
|
149
|
+
const sibling = at % 2 ? at - 1 : at + 1;
|
|
150
|
+
if (sibling < level.length)
|
|
151
|
+
path.push(Buffer.from(level[sibling]).toString("hex"));
|
|
152
|
+
at = Math.floor(at / 2);
|
|
153
|
+
}
|
|
154
|
+
return { rotation: jcsHash(rotation), licence: hash, index, size: snapshot.hashes.length, path };
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
export const licenceOwnerProof = (rotation, licences, hash) => licenceOwnerProofs(rotation, licences, [hash])[0];
|
|
158
|
+
export function verifyLicenceWithOwnerHistory(licence, opts) {
|
|
159
|
+
const denied = (reason) => ({ ok: false, reasons: [reason] });
|
|
160
|
+
if (!isObj(opts) || Object.keys(opts).length !== 4 || !Array.isArray(opts.licenceProofs) || opts.licenceProofs.length > 1000 || !Array.from(opts.licenceProofs).every(proofShape)
|
|
161
|
+
|| !withinVerificationBudget({ licence, opts }))
|
|
162
|
+
return denied("rotated-owner policy or proofs exceed bounds");
|
|
163
|
+
const checked = verifyLicenceOwnerHistory(opts.log, opts);
|
|
164
|
+
if (!checked.ok)
|
|
165
|
+
return { ok: false, reasons: checked.reasons };
|
|
166
|
+
if (!isObj(licence) || !did(licence.signer) || !isIso(licence.issued_at))
|
|
167
|
+
return denied("invalid licence owner or issuance time");
|
|
168
|
+
const owner = checked.owners.find((o) => o.did === licence.signer), outgoing = checked.rotations.find((r) => r.from === licence.signer);
|
|
169
|
+
if (!owner || (owner.from && ms(licence.issued_at) < ms(owner.from)) || (outgoing && ms(licence.issued_at) > ms(outgoing.at)))
|
|
170
|
+
return denied("licence was not signed by an owner in force at issuance");
|
|
171
|
+
const reasons = verifyLicence(licence, { owner: licence.signer }).reasons;
|
|
172
|
+
if (outgoing) {
|
|
173
|
+
const hash = jcsHash(licence), rotation = jcsHash(outgoing), proof = opts.licenceProofs.find((p) => p.licence === hash && p.rotation === rotation);
|
|
174
|
+
if (!proof)
|
|
175
|
+
reasons.push("retired-owner licence needs its cutover issuance inclusion proof");
|
|
176
|
+
else if (proof.size !== outgoing.licences.size || !verifyInclusion(licenceLeaf(hash), proof.index, proof.size, proof.path.map((p) => Buffer.from(p, "hex")), Buffer.from(outgoing.licences.root.slice(7), "hex")))
|
|
177
|
+
reasons.push("retired-owner licence is not in the cutover issuance snapshot");
|
|
178
|
+
}
|
|
179
|
+
return { ok: reasons.length === 0, reasons };
|
|
180
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { Credential } from "@zanii/credentials";
|
|
2
|
+
import { type BodySignature } from "./board.ts";
|
|
3
|
+
import { type Signer } from "./signer.ts";
|
|
4
|
+
import { type LicenceOwnerHistoryPolicy } from "./licence-owner.ts";
|
|
5
|
+
/** Section 1: the largest royalty per certificate, and section 4 the most certificates in one statement (their product stays a safe integer). */
|
|
6
|
+
export declare const MAX_ROYALTY_MINOR = 1000000000;
|
|
7
|
+
export declare const MAX_STATEMENT_CERTIFICATES = 1000000;
|
|
8
|
+
export interface LicenceScope {
|
|
9
|
+
profile: string;
|
|
10
|
+
levels: number[];
|
|
11
|
+
}
|
|
12
|
+
export interface Licence {
|
|
13
|
+
v: 1;
|
|
14
|
+
type: "certify.licence";
|
|
15
|
+
licensee: string;
|
|
16
|
+
legal_name: string;
|
|
17
|
+
countries: string[];
|
|
18
|
+
scope: LicenceScope[];
|
|
19
|
+
valid_from: string;
|
|
20
|
+
valid_until: string;
|
|
21
|
+
royalty: {
|
|
22
|
+
per_certificate_minor: number;
|
|
23
|
+
currency: string;
|
|
24
|
+
};
|
|
25
|
+
issued_at: string;
|
|
26
|
+
signer: string;
|
|
27
|
+
sig: string;
|
|
28
|
+
}
|
|
29
|
+
export interface LicenceInput {
|
|
30
|
+
licensee: string;
|
|
31
|
+
legalName: string;
|
|
32
|
+
countries: readonly string[];
|
|
33
|
+
scope: readonly {
|
|
34
|
+
profile: string;
|
|
35
|
+
levels: readonly number[];
|
|
36
|
+
}[];
|
|
37
|
+
validFrom: string;
|
|
38
|
+
validUntil: string;
|
|
39
|
+
royalty: {
|
|
40
|
+
per_certificate_minor: number;
|
|
41
|
+
currency: string;
|
|
42
|
+
};
|
|
43
|
+
issuedAt: string;
|
|
44
|
+
}
|
|
45
|
+
export interface LicenceCheck {
|
|
46
|
+
ok: boolean;
|
|
47
|
+
reasons: string[];
|
|
48
|
+
}
|
|
49
|
+
/** A scope list (licences, the body register): 1 to 256 `{profile, levels}`, sorted by profile, each once. */
|
|
50
|
+
export declare function scopeError(s: unknown): string | null;
|
|
51
|
+
/** Sorted by profile, each profile's levels sorted and distinct (the shape `scopeError` accepts). */
|
|
52
|
+
export declare const normaliseScope: (scope: readonly {
|
|
53
|
+
profile: string;
|
|
54
|
+
levels: readonly number[];
|
|
55
|
+
}[]) => LicenceScope[];
|
|
56
|
+
/** Section 1: a licence, signed by the scheme owner (`signer`). */
|
|
57
|
+
export declare function buildLicence(f: LicenceInput, signer: Signer): Promise<Licence>;
|
|
58
|
+
/** Section 1: signed, well formed and (with `owner`) signed by the scheme owner. */
|
|
59
|
+
export declare function verifyLicence(doc: unknown, opts?: {
|
|
60
|
+
owner?: string;
|
|
61
|
+
}): LicenceCheck;
|
|
62
|
+
/** Section 3: is `issuer` the licensee, or a key the licensee's key was succeeded by in `lineage` (oldest first)? */
|
|
63
|
+
export declare const licensedKey: (licensee: unknown, issuer: unknown, lineage?: readonly string[]) => boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Section 3: what a licence must cover for a certificate: its issuer (the licensee, or a later key
|
|
66
|
+
* of the licensee's verified `lineage`), profile, level, country and issue time.
|
|
67
|
+
*/
|
|
68
|
+
export declare function licenceCoverage(licence: Record<string, unknown>, cert: Credential, lineage?: readonly string[]): string[];
|
|
69
|
+
export declare const LICENCE_ACTIONS: readonly ["suspend", "reinstate", "withdraw"];
|
|
70
|
+
export type LicenceAction = (typeof LICENCE_ACTIONS)[number];
|
|
71
|
+
export declare const LICENCE_REASONS: readonly ["ROYALTY_UNPAID", "SCOPE_BREACH", "MARK_MISUSE", "AUDIT_FINDING", "ACCREDITATION_LOST", "LICENSEE_REQUEST", "OTHER"];
|
|
72
|
+
export type LicenceReason = (typeof LICENCE_REASONS)[number];
|
|
73
|
+
export type LicenceState = "active" | "suspended" | "withdrawn";
|
|
74
|
+
export interface LicenceStatus {
|
|
75
|
+
v: 1;
|
|
76
|
+
type: "certify.licence-status";
|
|
77
|
+
seq: number;
|
|
78
|
+
prev: string | null;
|
|
79
|
+
licence: string;
|
|
80
|
+
action: LicenceAction;
|
|
81
|
+
reason: LicenceReason | null;
|
|
82
|
+
at: string;
|
|
83
|
+
/** Section 2a: a suspension or withdrawal for cause; certificates issued from `effective_at` are refused. */
|
|
84
|
+
for_cause?: true;
|
|
85
|
+
effective_at?: string;
|
|
86
|
+
signer: string;
|
|
87
|
+
sig: string;
|
|
88
|
+
}
|
|
89
|
+
declare function statusError(s: Record<string, unknown>): string | null;
|
|
90
|
+
export { statusError as licenceStatusFieldError };
|
|
91
|
+
/**
|
|
92
|
+
* Section 2: one licence status change, signed by the scheme owner. With `effectiveAt` (section 2a)
|
|
93
|
+
* a suspension or withdrawal is for cause from that time.
|
|
94
|
+
*/
|
|
95
|
+
export declare function buildLicenceStatus(f: {
|
|
96
|
+
seq: number;
|
|
97
|
+
prev: string | null;
|
|
98
|
+
licence: string;
|
|
99
|
+
action: LicenceAction;
|
|
100
|
+
reason: LicenceReason | null;
|
|
101
|
+
at: string;
|
|
102
|
+
effectiveAt?: string;
|
|
103
|
+
}, signer: Signer): Promise<LicenceStatus>;
|
|
104
|
+
/**
|
|
105
|
+
* Section 2: one log, signed by one key (with `owner`, the scheme owner), at seq i naming entry
|
|
106
|
+
* i - 1, `at` never going back, and every change allowed from the licence's state (withdrawal is final).
|
|
107
|
+
*/
|
|
108
|
+
export declare function verifyLicenceLog(log: readonly unknown[], opts?: {
|
|
109
|
+
owner?: string;
|
|
110
|
+
}): LicenceCheck;
|
|
111
|
+
/** Section 2: a licence's state at `at` from a verified log (changes at or before `at` apply). */
|
|
112
|
+
export declare function licenceStateAt(log: readonly LicenceStatus[], licence: string, at: string): LicenceState;
|
|
113
|
+
/** `verifyCertificate`'s `licences` option (section 3). */
|
|
114
|
+
export interface LicenceOptions {
|
|
115
|
+
/** The scheme owner's did:key: licences and the licence status log must be signed by it. */
|
|
116
|
+
owner: string;
|
|
117
|
+
/** Candidate licences; the one whose jcsHash is `claim.licence` is checked. */
|
|
118
|
+
licences: readonly unknown[];
|
|
119
|
+
/** The scheme owner's licence status log; without it suspension and withdrawal are not checked. */
|
|
120
|
+
log?: readonly unknown[];
|
|
121
|
+
/** Issuer DIDs of the scheme owner's own labs, which certify without a licence. */
|
|
122
|
+
ownIssuers?: readonly string[];
|
|
123
|
+
/** The certificate issuer's verified key lineage, oldest first (`issuerLineage`): a licence naming an earlier key covers a later one. */
|
|
124
|
+
lineage?: readonly string[];
|
|
125
|
+
/** Strict rotated-owner history and independently pinned checkpoint (licensing.md section 6). */
|
|
126
|
+
ownerHistory?: LicenceOwnerHistoryPolicy;
|
|
127
|
+
}
|
|
128
|
+
/** Section 3: the reasons a certificate is not covered by a licence that was valid when it was issued. */
|
|
129
|
+
export declare function licenceReasons(cert: Credential, opts: LicenceOptions): string[];
|
|
130
|
+
export interface RoyaltyStatementInput {
|
|
131
|
+
licence: Licence;
|
|
132
|
+
period: {
|
|
133
|
+
from: string;
|
|
134
|
+
until: string;
|
|
135
|
+
};
|
|
136
|
+
/** The hashes of the certificates issued under the licence in the period. */
|
|
137
|
+
certificates: readonly string[];
|
|
138
|
+
issuedAt: string;
|
|
139
|
+
}
|
|
140
|
+
export type RoyaltyStatementBody = {
|
|
141
|
+
v: 1;
|
|
142
|
+
type: "certify.royalty-statement";
|
|
143
|
+
licence: string;
|
|
144
|
+
licensee: string;
|
|
145
|
+
period: {
|
|
146
|
+
from: string;
|
|
147
|
+
until: string;
|
|
148
|
+
};
|
|
149
|
+
certificates: number;
|
|
150
|
+
certificates_sha256: string;
|
|
151
|
+
amount_minor: number;
|
|
152
|
+
currency: string;
|
|
153
|
+
issued_at: string;
|
|
154
|
+
};
|
|
155
|
+
export type RoyaltyStatement = RoyaltyStatementBody & {
|
|
156
|
+
signatures: BodySignature[];
|
|
157
|
+
};
|
|
158
|
+
/** Section 4: `jcsHash({items})` of the sorted, distinct certificate hashes. */
|
|
159
|
+
export declare const certificateListHash: (certificates: readonly string[]) => string;
|
|
160
|
+
/** Section 4: the statement signed by the licensee (`signer`, the licence's licensee key). */
|
|
161
|
+
export declare function buildRoyaltyStatement(f: RoyaltyStatementInput, signer: Signer): Promise<RoyaltyStatement>;
|
|
162
|
+
/** Section 4: the scheme owner's countersignature, added beside the licensee's. */
|
|
163
|
+
export declare function countersignRoyaltyStatement(statement: RoyaltyStatement, signer: Signer): Promise<RoyaltyStatement>;
|
|
164
|
+
/**
|
|
165
|
+
* Section 4: signed by its licensee, countersigned by `owner`, and (with `licence`) the amount is
|
|
166
|
+
* the certificate count times the licence royalty in its currency; with `certificates`, the count
|
|
167
|
+
* and list hash are those of the supplied hashes.
|
|
168
|
+
*/
|
|
169
|
+
export declare function verifyRoyaltyStatement(doc: unknown, opts?: {
|
|
170
|
+
owner?: string;
|
|
171
|
+
licence?: unknown;
|
|
172
|
+
certificates?: readonly string[];
|
|
173
|
+
ownerHistory?: LicenceOwnerHistoryPolicy;
|
|
174
|
+
}): LicenceCheck;
|