@meantjs/profile 0.0.0-stage → 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.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * `@meantjs/profile/browser`: the resolver without Node built-ins, for a browser bundle. The same functions as
3
+ * the main entry, except the ones that read files (`loadPacks`, the CLI, the default paths): the schema and the
4
+ * standard packs come in as the data this package ships, and `createSchemaValidator` takes a document, never a
5
+ * path. The Candid compiler is the `@candid-core/cli` wasm; it initializes itself on first use (fetched next to the
6
+ * module), or call `initCompiler(urlOrBytes)` first when a bundler moves the wasm file.
7
+ *
8
+ * test/browser-entry.test.mjs keeps the module graph free of node:* imports and checks the data against data/.
9
+ */
10
+ import schema from "../data/profile-v0.schema.json" with { type: "json" };
11
+ import icrc1 from "../data/packs/icrc1.profile.json" with { type: "json" };
12
+ import icrc2 from "../data/packs/icrc2.profile.json" with { type: "json" };
13
+ import erc20 from "../data/packs/erc20.profile.json" with { type: "json" };
14
+ import { compileSchema } from "./schema-core.mjs";
15
+
16
+ export { init as initCompiler } from "@candid-core/cli";
17
+ export { idlHash, jcs, profileId } from "./util.mjs";
18
+ export { parseAddress, parseRelPath } from "./address.mjs";
19
+ export { Iface } from "./iface.mjs";
20
+ export { TagIndex, LayeredTags, positions } from "./tags.mjs";
21
+ export { schemaDiagnostics } from "./schema-core.mjs";
22
+ export { compileInterface, interfaceFromEnvelope } from "./compile.mjs";
23
+ export { validateProfile } from "./validate.mjs";
24
+ export { resolveService } from "./resolve.mjs";
25
+ export { matchPack } from "./pack-match.mjs";
26
+ export { matchService, typeSubtype, SubtypeChecker } from "./subtype.mjs";
27
+ export { analyzeProfiles, LAYERS } from "./analyze.mjs";
28
+ export { resolvedForm, buildTree } from "./resolved.mjs";
29
+ export { profileFromDocAttributes, DOC_ATTRIBUTES_VERSION } from "./doc-annotations.mjs";
30
+ export { readServedProfile, SERVED_SECTION, SERVED_METADATA, SERVED_MAX_BYTES } from "./served.mjs";
31
+
32
+ /** The profile schema (a copy of spec/profile-v0.schema.json). */
33
+ export const PROFILE_SCHEMA = schema;
34
+
35
+ /** The standard packs (copies of packs/*.profile.json), sorted by file name, in the shape `loadPacks` returns. */
36
+ export const STANDARD_PACKS = Object.freeze([
37
+ { file: "erc20.profile.json", doc: erc20 },
38
+ { file: "icrc1.profile.json", doc: icrc1 },
39
+ { file: "icrc2.profile.json", doc: icrc2 },
40
+ ]);
41
+
42
+ /**
43
+ * The validate function for the profile schema. The schema this package ships (the default, or any document equal to
44
+ * it) is compiled ahead of time and generates no code at run time, so it needs no 'unsafe-eval' in a page's
45
+ * Content-Security-Policy. Any other document is compiled by ajv with `new Function`, which does.
46
+ *
47
+ * @param doc the schema document (default: the copy this package ships)
48
+ */
49
+ export async function createSchemaValidator(doc = schema) {
50
+ if (typeof doc === "string") throw new TypeError("createSchemaValidator: the browser entry takes the schema document, not a path");
51
+ return compileSchema(doc);
52
+ }
package/src/checks.mjs ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Per-annotation semantic checks (spec sections 3 to 6). Moved verbatim from scripts/validate.mjs.
3
+ */
4
+ import { parseRelPath } from "./address.mjs";
5
+
6
+ /** EVM integer types (spec section 15): uint8 to uint256 and int8 to int256. int8 to int64 are also Candid's, with the same bounds. */
7
+ const ABI_BITS = Array.from({ length: 32 }, (_, i) => 8 * (i + 1));
8
+ export const NUMERIC = new Set(["nat", "int", "nat8", "nat16", "nat32", "nat64", "int8", "int16", "int32", "int64", ...ABI_BITS.flatMap((b) => [`uint${b}`, `int${b}`])]);
9
+ export const TIMEISH = new Set(["nat", "int", "nat32", "nat64", "int64", ...ABI_BITS.filter((b) => b >= 32).map((b) => `uint${b}`), ...ABI_BITS.filter((b) => b >= 64).map((b) => `int${b}`)]);
10
+ /** Text: Candid's `text`, the ABI's `string`. */
11
+ export const TEXT = new Set(["text", "string"]);
12
+ /** Tags an EVM contract profile cannot use yet: they need receipts, nonces or reads that phase 2 adds (spec section 15). */
13
+ export const EVM_LATER_TAGS = new Set(["idempotency", "outcome", "list", "names", "named"]);
14
+ export const METHOD_TAGS = new Set(["intent", "effect", "idempotency", "outcome", "list", "names"]);
15
+ /** Method tags that may also sit on a case of a variant in the arguments (spec 5.2, D-053). */
16
+ export const CASE_TAGS = new Set(["intent", "effect"]);
17
+ /** Effect kinds that need a person's confirmation (spec 5.3). */
18
+ export const CONFIRM_KINDS = new Set(["transfer", "approve", "admin", "destroy"]);
19
+ /** Tags that convert a value between its wire form and a human form; a stale profile must not supply them (spec section 7). */
20
+ export const VALUE_CONVERSION_TAGS = new Set(["amount", "timestamp", "duration", "account", "subaccount", "bytes", "named"]);
21
+ export const STALE_CODES = new Set(["E_ADDR_UNRESOLVED", "E_TYPE_NODE", "E_TAG_NOT_APPLICABLE", "E_REF_UNRESOLVED", "E_REF_NOT_QUERY", "E_REF_SHAPE", "E_REF_TYPE", "E_STAMP_UNTAGGED", "E_OUTCOME_NOT_CASE", "E_TEMPLATE_PATH", "E_TAG_TARGET", "E_NAMES_MODE", "E_NAMES_SHAPE", "E_NAMES_FIELD", "E_NAMED_SET", "E_NAMED_TYPE", "E_CASE_OVERLAP", "E_CASE_EFFECT"]);
22
+
23
+ export function checkApplicability(iface, tag, ref) {
24
+ const { ref: r } = iface.unwrapOpt(ref);
25
+ const p = iface.prim(r);
26
+ if (iface.chain === "evm" && (tag === "account" || tag === "subaccount")) return `${tag} is an ICRC type; an EVM address needs no tag (it is shown with its EIP-55 checksum, spec section 15)`;
27
+ switch (tag) {
28
+ case "amount": return NUMERIC.has(p) ? null : `amount needs a numeric type, found ${iface.describeType(ref)}`;
29
+ case "timestamp":
30
+ case "duration": return TIMEISH.has(p) ? null : `${tag} needs ${iface.chain === "evm" ? "uint32 to uint256 or int64 to int256" : "nat/int/nat32/nat64/int64"}, found ${iface.describeType(ref)}`;
31
+ case "account": return iface.isAccount(r) ? null : `account needs record { owner : principal; subaccount : opt blob }, found ${iface.describeType(ref)}`;
32
+ case "subaccount":
33
+ case "bytes": return iface.isBlob(r) ? null : `${tag} needs ${iface.chain === "evm" ? "bytes or bytesN" : "blob"}, found ${iface.describeType(ref)}`;
34
+ case "text": return TEXT.has(p) ? null : `text needs ${iface.chain === "evm" ? "string" : "text"}, found ${iface.describeType(ref)}`;
35
+ case "range": return NUMERIC.has(p) || TEXT.has(p) || iface.node(r).kind === "vec" ? null : `range needs numeric, text or vec, found ${iface.describeType(ref)}`;
36
+ default: return null;
37
+ }
38
+ }
39
+
40
+ export function checkDynamic(iface, value, want, diag, at) {
41
+ if (!value || typeof value !== "object") return;
42
+ const from = value.from;
43
+ if (from === "now" || from === "caller") {
44
+ if (want !== "any") diag("error", "E_DYNAMIC_KIND", at, `"${from}" is not valid here; expected query:<method> returning ${want}`);
45
+ return;
46
+ }
47
+ const name = from.slice("query:".length);
48
+ const m = iface.methods.get(name);
49
+ if (!m) return diag("error", "E_REF_UNRESOLVED", at, `dynamic ref "${from}": no method "${name}" in the interface`);
50
+ if (m.mode !== "query" && m.mode !== "composite_query") diag("error", "E_REF_NOT_QUERY", at, `dynamic ref "${from}" must be a query method`);
51
+ if (m.args.length !== 0 || m.results.length !== 1) diag("error", "E_REF_SHAPE", at, `dynamic ref "${from}" must take no arguments and return one value`);
52
+ else {
53
+ const p = iface.prim(iface.unwrapOpt(m.results[0]).ref);
54
+ if (want === "numeric" && !NUMERIC.has(p)) diag("error", "E_REF_TYPE", at, `"${from}" returns ${p}, expected a number`);
55
+ if (want === "text" && !TEXT.has(p)) diag("error", "E_REF_TYPE", at, `"${from}" returns ${p}, expected text`);
56
+ }
57
+ }
58
+
59
+ export function parseTemplate(t) {
60
+ const placeholders = [];
61
+ let depth = 0;
62
+ let segments = 0;
63
+ for (let i = 0; i < t.length; i++) {
64
+ const c = t[i];
65
+ if ((c === "{" || c === "}" || c === "[" || c === "]") && t[i + 1] === c) { i++; continue; } // escaped
66
+ if (c === "[") { if (depth) return { error: "nested [ ] segments are not allowed in v0" }; depth = 1; segments++; }
67
+ else if (c === "]") { if (!depth) return { error: "unbalanced ]" }; depth = 0; }
68
+ else if (c === "{") {
69
+ const end = t.indexOf("}", i);
70
+ if (end < 0) return { error: "unclosed {" };
71
+ placeholders.push({ path: t.slice(i + 1, end), inOptional: depth === 1 });
72
+ i = end;
73
+ } else if (c === "}") return { error: "unbalanced }" };
74
+ }
75
+ if (depth) return { error: "unclosed [" };
76
+ return { placeholders, segments };
77
+ }
78
+
79
+ export function outcomeCoverage(iface, methodName, classes, diag, at, hasDefault) {
80
+ const m = iface.methods.get(methodName);
81
+ if (m.results.length !== 1) return;
82
+ const visit = (ref, prefix) => {
83
+ const { ref: r } = iface.unwrapOpt(ref);
84
+ const n = iface.node(r);
85
+ if (n.kind !== "variant") return;
86
+ for (const f of n.fields) {
87
+ const key = `${prefix}${iface.fieldName(r, f.id)}`;
88
+ if (classes[key]) continue;
89
+ const inner = iface.node(iface.unwrapOpt(f.type).ref);
90
+ if (inner.kind === "variant" && Object.keys(classes).some((k) => k.startsWith(`${key}.`))) visit(f.type, `${key}.`);
91
+ else diag(hasDefault ? "info" : "warning", "W_OUTCOME_UNCLASSIFIED", at, `result case "${key}" has no class${hasDefault ? " (falls back to default)" : ""}`);
92
+ }
93
+ };
94
+ visit(m.results[0], "");
95
+ }
96
+
97
+ /** The effect a method has without an effect tag (spec 5.3): queries read, updates write irreversibly. */
98
+ export function defaultEffect(mode) {
99
+ return mode === "query" || mode === "composite_query" ? { kind: "read", irreversible: false } : { kind: "write", irreversible: true };
100
+ }
101
+
102
+ /** Why an intent or effect at a value address is not on a case of an argument (spec 5.2, D-053), or null. */
103
+ export function caseTargetError(iface, addr, res, tag) {
104
+ if (addr.kind === "type") return `${tag} sits on a method or on a case of a variant in its arguments, never at a type: address (it can reach several methods)`;
105
+ if (addr.kind === "result") return `${tag} sits on a method or on a case of a variant in its arguments, not in its results`;
106
+ if (addr.segs.some((s) => s.kind === "elem")) return `${tag} on a case cannot step into a vector element ([]): one call holds many elements`;
107
+ if (res.parent?.kind !== "variant") return `${tag} sits on a method or on a case of a variant in its arguments; this address ends on ${res.parent ? "a record field" : "a whole argument"}`;
108
+ return null;
109
+ }
110
+
111
+ /**
112
+ * Walk two segment lists of one method argument side by side. Returns where they part: { kind } of the container at
113
+ * the first differing step, with the two labels, or null when one is a prefix of the other.
114
+ */
115
+ function part(iface, start, a, b) {
116
+ let cur = start;
117
+ for (let k = 0; k < Math.min(a.length, b.length); k++) {
118
+ cur = iface.unwrapOpt(cur).ref;
119
+ const n = iface.node(cur);
120
+ const x = a[k];
121
+ const y = b[k];
122
+ if (x.kind === "elem" || y.kind === "elem") {
123
+ if (x.kind === y.kind) { cur = n.inner; continue; }
124
+ return { kind: n.kind };
125
+ }
126
+ if (x.id !== y.id) return { kind: n.kind, a: x.label, b: y.label };
127
+ cur = n.fields.find((f) => f.id === x.id).type;
128
+ }
129
+ return null;
130
+ }
131
+
132
+ /**
133
+ * A case template's placeholder that selects another case of a variant the case address passes through, so it can
134
+ * never be present (spec section 6, D-053): the reason, or null. Both paths resolve; `rel` is a method-tuple relpath.
135
+ */
136
+ export function otherCase(iface, caseAddr, rel) {
137
+ const p = parseRelPath(rel);
138
+ const index = p.index ?? 0;
139
+ if (index !== caseAddr.index) return null;
140
+ const m = iface.methods.get(caseAddr.method);
141
+ const at = part(iface, m.args[index], caseAddr.segs, p.segs);
142
+ return at?.kind === "variant" ? `selects case "${at.b}" of a variant where this intent's case takes "${at.a}", so it is never present` : null;
143
+ }
144
+
145
+ /**
146
+ * Whether one call can take two tagged cases of one method (spec 5.2, D-053): the reason, or null when they exclude
147
+ * each other (they part at a variant) or nest.
148
+ */
149
+ export function overlap(iface, a, b) {
150
+ if (a.index !== b.index) return "they sit in different arguments";
151
+ const m = iface.methods.get(a.method);
152
+ const at = part(iface, m.args[a.index], a.segs, b.segs);
153
+ return at && at.kind !== "variant" ? "their paths part at a record field" : null;
154
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * candid-profile resolve (<service.did> | --envelope <file> | --abi <file>) [--profile p.json ...] [--packs] [--packs-dir dir]
3
+ *
4
+ * Prints the resolved form (docs/profile-v0.md section 10) on stdout and a summary on stderr.
5
+ * Exit code: 0 when no diagnostic is an error, 1 when one is (the resolved form is still printed when the service
6
+ * compiles), 2 for a usage or I/O problem.
7
+ */
8
+ import { readFile } from "node:fs/promises";
9
+ import path from "node:path";
10
+ import { createSchemaValidator } from "./schema.mjs";
11
+ import { DEFAULT_PACKS_DIR, loadPacks } from "./packs.mjs";
12
+ import { resolveService } from "./resolve.mjs";
13
+
14
+ export const USAGE = `usage: candid-profile resolve <service.did> [options] > resolved.json
15
+ candid-profile resolve --envelope <file> [options] > resolved.json
16
+ candid-profile resolve --abi <file> [options] > resolved.json
17
+
18
+ --envelope <file> the service's candid-core ContractEnvelope (as \`candid-core-cli gen\` writes it), instead of
19
+ compiling a .did here
20
+ --abi <file> an EVM contract's ABI, a JSON array of human-readable fragments or JSON ABI entries
21
+ (experimental, spec section 15)
22
+ --profile <file> a profile to apply (repeatable). A pack given this way must match the service.
23
+ --packs try every pack in the packs directory; apply those that match
24
+ --packs-dir <dir> packs directory (default: the standard packs this package ships)
25
+ --verbose also list info diagnostics on stderr
26
+ --quiet no summary on stderr
27
+ -h, --help this text
28
+ `;
29
+
30
+ class UsageError extends Error {}
31
+
32
+ function parseArgs(argv) {
33
+ const opts = { did: null, envelope: null, abi: null, profiles: [], packs: false, packsDir: DEFAULT_PACKS_DIR, verbose: false, quiet: false, help: false };
34
+ const args = [...argv];
35
+ const value = (name, inline) => {
36
+ if (inline !== undefined) return inline;
37
+ const v = args.shift();
38
+ if (v === undefined || v.startsWith("--")) throw new UsageError(`${name} needs a value`);
39
+ return v;
40
+ };
41
+ while (args.length) {
42
+ const a = args.shift();
43
+ const [flag, inline] = a.startsWith("--") && a.includes("=") ? [a.slice(0, a.indexOf("=")), a.slice(a.indexOf("=") + 1)] : [a, undefined];
44
+ if (flag === "--profile") opts.profiles.push(value(flag, inline));
45
+ else if (flag === "--envelope") opts.envelope = value(flag, inline);
46
+ else if (flag === "--abi") opts.abi = value(flag, inline);
47
+ else if (flag === "--packs") opts.packs = true;
48
+ else if (flag === "--packs-dir") opts.packsDir = value(flag, inline);
49
+ else if (flag === "--verbose") opts.verbose = true;
50
+ else if (flag === "--quiet") opts.quiet = true;
51
+ else if (flag === "-h" || flag === "--help") opts.help = true;
52
+ else if (a.startsWith("-")) throw new UsageError(`unknown option ${a}`);
53
+ else if (opts.did === null) opts.did = a;
54
+ else throw new UsageError(`unexpected argument ${a}`);
55
+ }
56
+ return opts;
57
+ }
58
+
59
+ async function readJsonFile(file, what) {
60
+ try {
61
+ return JSON.parse(await readFile(file, "utf8"));
62
+ } catch (e) {
63
+ throw new UsageError(`cannot read ${what} ${file}: ${e.message}`);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * @param argv arguments after the program name, starting with the command
69
+ * @param io { stdout, stderr } with write(string)
70
+ * @returns the exit code
71
+ */
72
+ export async function main(argv, io) {
73
+ const err = (s) => io.stderr.write(`${s}\n`);
74
+ try {
75
+ const [command, ...rest] = argv;
76
+ if (command === undefined || command === "-h" || command === "--help") { err(USAGE.trimEnd()); return command === undefined ? 2 : 0; }
77
+ if (command !== "resolve") throw new UsageError(`unknown command ${command}`);
78
+ const opts = parseArgs(rest);
79
+ if (opts.help) { err(USAGE.trimEnd()); return 0; }
80
+ const given = [opts.did, opts.envelope, opts.abi].filter((x) => x !== null).length;
81
+ if (given > 1) throw new UsageError("give only one of <service.did>, --envelope and --abi");
82
+ if (given === 0) throw new UsageError("missing <service.did> (or --envelope <file>, or --abi <file>)");
83
+ const input = {};
84
+ if (opts.envelope) input.envelope = await readJsonFile(opts.envelope, "envelope");
85
+ else if (opts.abi) input.abi = await readJsonFile(opts.abi, "ABI");
86
+ else {
87
+ try { input.did = await readFile(opts.did, "utf8"); } catch (e) { throw new UsageError(`cannot read ${opts.did}: ${e.message}`); }
88
+ }
89
+
90
+ const validateSchema = await createSchemaValidator();
91
+ const profiles = [];
92
+ for (const f of opts.profiles) profiles.push({ doc: await readJsonFile(f, "profile"), label: f });
93
+ const packs = [];
94
+ if (opts.packs) {
95
+ let found;
96
+ try { found = await loadPacks(opts.packsDir); } catch (e) { throw new UsageError(`cannot read packs from ${opts.packsDir}: ${e.message}`); }
97
+ if (!found.length) throw new UsageError(`no packs (*.profile.json of kind pack) in ${opts.packsDir}`);
98
+ for (const p of found) packs.push({ doc: p.doc, label: path.relative(process.cwd(), p.file) });
99
+ }
100
+
101
+ const r = await resolveService({ ...input, didLabel: opts.did ?? opts.envelope ?? opts.abi, validateSchema, profiles, packs });
102
+ if (r.resolved) io.stdout.write(`${JSON.stringify(r.resolved, null, 2)}\n`);
103
+ if (!opts.quiet) summary(r, opts, err);
104
+ return r.ok ? 0 : 1;
105
+ } catch (e) {
106
+ if (e instanceof UsageError) { err(`candid-profile: ${e.message}`); err(USAGE.trimEnd()); return 2; }
107
+ throw e;
108
+ }
109
+ }
110
+
111
+ function summary(r, opts, err) {
112
+ const count = (s) => r.diagnostics.filter((d) => d.severity === s).length;
113
+ if (r.resolved) err(`candid-profile: ${opts.did ?? opts.envelope ?? opts.abi} ${r.resolved.interface_id} [${Object.keys(r.resolved.methods).length} methods]`);
114
+ for (const p of r.profiles) err(` ${p.status.padEnd(11)} ${String(p.name).padEnd(16)} ${String(p.origin ?? "?").padEnd(12)} ${p.profile_id ?? ""}`);
115
+ err(` ${count("error")} error, ${count("warning")} warning, ${count("info")} info`);
116
+ for (const d of r.diagnostics) {
117
+ if (d.severity === "info" && !opts.verbose) continue;
118
+ err(` ${d.severity.padEnd(7)} ${d.code.padEnd(26)} ${d.profile ? `[${d.profile}] ` : ""}${d.at} ${d.message}`);
119
+ }
120
+ }
@@ -0,0 +1,304 @@
1
+ import { didToContract } from "@candid-core/cli";
2
+ import { Iface } from "./iface.mjs";
3
+ import { U32_MAX, idlHash } from "./idl.mjs";
4
+
5
+ /**
6
+ * Compile Candid source text to an Iface with @candid-core/cli (wasm).
7
+ * @returns { iface } or { failure: [candid-core diagnostics] }
8
+ */
9
+ export async function compileInterface(source, label = "interface") {
10
+ const env = await didToContract(source);
11
+ if (env.ok === false) return { failure: env.diagnostics };
12
+ return { iface: new Iface(env, label) };
13
+ }
14
+
15
+ /**
16
+ * An Iface from a candid-core ContractEnvelope another tool already wrote (the one `candid-core-cli gen` writes next
17
+ * to a generated module), without compiling the .did again. The envelope is checked for the shape the resolver reads;
18
+ * its identities are taken as written (nothing here can recompute them).
19
+ * @returns { iface } or { failure: [{ code, message }] }
20
+ */
21
+ export function interfaceFromEnvelope(envelope, label = "interface") {
22
+ const fail = (message) => ({ failure: [{ code: "invalid_envelope", message }] });
23
+ if (envelope === null || typeof envelope !== "object" || Array.isArray(envelope)) return fail("not a JSON object");
24
+ const c = envelope.contract;
25
+ if (c === null || typeof c !== "object" || Array.isArray(c)) return fail("no `contract` object (is this a candid-core ContractEnvelope?)");
26
+ if (c.format !== "candid-core") return fail(`contract.format is ${JSON.stringify(c.format)}, expected "candid-core"`);
27
+ if (c.format_version !== 1) return fail(`contract.format_version is ${JSON.stringify(c.format_version)}, this resolver reads version 1`);
28
+ const id = c.identities?.interface;
29
+ if (typeof id !== "string" || !/^candid-core:interface:v1:sha256:[0-9a-f]{64}$/.test(id)) return fail("no contract.identities.interface of the form candid-core:interface:v1:sha256:<64 lowercase hex>");
30
+ // the Candid semantics and the canonical interning this resolver implements; the interface id covers both markers
31
+ if (c.semantics_profile !== "candid-1") return fail(`contract.semantics_profile is ${JSON.stringify(c.semantics_profile)}, this resolver implements "candid-1"`);
32
+ if (c.canonicalization_profile !== "candid-core-canon-1") return fail(`contract.canonicalization_profile is ${JSON.stringify(c.canonicalization_profile)}, this resolver implements "candid-core-canon-1"`);
33
+ if (!Array.isArray(c.types) || !Array.isArray(c.declarations)) return fail("contract.types and contract.declarations must be arrays");
34
+ const names = envelope.extensions?.["org.candid-core.field-names/v1"];
35
+ if (!Array.isArray(names)) return fail("no extensions[\"org.candid-core.field-names/v1\"] array: without it every field and case would be renamed to its id");
36
+ const bad = badGraph(c, names);
37
+ if (bad) return fail(bad);
38
+ try {
39
+ return { iface: new Iface(envelope, label) };
40
+ } catch (e) {
41
+ return fail(`the contract graph cannot be read: ${e.message}`);
42
+ }
43
+ }
44
+
45
+ const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
46
+ const KINDS = new Set(["primitive", "opt", "vec", "record", "variant", "func", "service", "class"]);
47
+ const MODES = new Set(["update", "query", "composite_query", "oneway"]);
48
+ const PRIMITIVES = new Set([
49
+ "nat", "nat8", "nat16", "nat32", "nat64", "int", "int8", "int16", "int32", "int64",
50
+ "float32", "float64", "bool", "text", "null", "reserved", "empty", "principal",
51
+ ]);
52
+
53
+ /**
54
+ * The first thing in the contract graph this resolver cannot read, as a readable sentence; null if none. Checked
55
+ * here, before anything walks the graph, so a malformed envelope is an E_ENVELOPE diagnostic and never a throw or a
56
+ * loop further in: every reference is an index into `types`, declaration names are distinct strings, every node has the fields its kind needs, a method is
57
+ * a `func` with a mode (a oneway one with no results) and an id that is its name's hash, declarations are named by
58
+ * Candid identifiers, a class appears only as the actor, an actor names a `service` or `class`, every node is reached from the actor
59
+ * or a declaration, no `opt` contains only itself, and every name in the field-names
60
+ * table is the name of a field its container has, with that id.
61
+ */
62
+ function badGraph(c, names) {
63
+ const n = c.types.length;
64
+ const ok = (r) => Number.isInteger(r) && r >= 0 && r < n;
65
+ const isKind = (r, kind) => ok(r) && c.types[r]?.kind === kind;
66
+ const refs = (a) => Array.isArray(a) && a.every(ok);
67
+ const declNames = new Set();
68
+ for (const [i, d] of c.declarations.entries()) {
69
+ if (!ok(d?.type)) return `dangling type reference: declarations[${i}].type`;
70
+ if (typeof d.name !== "string" || !IDENT.test(d.name)) return `declarations[${i}] needs a Candid identifier as its name, got ${JSON.stringify(d.name)}`;
71
+ if (declNames.has(d.name)) return `declaration ${JSON.stringify(d.name)} is listed twice`;
72
+ declNames.add(d.name);
73
+ }
74
+ for (const [i, t] of c.types.entries()) {
75
+ const at = `types[${i}]`;
76
+ if (t === null || typeof t !== "object" || !KINDS.has(t.kind)) return `${at} has no known kind`;
77
+ switch (t.kind) {
78
+ case "primitive":
79
+ if (!PRIMITIVES.has(t.primitive)) return `${at}: ${JSON.stringify(t.primitive)} is not a Candid primitive`;
80
+ break;
81
+ case "opt":
82
+ case "vec":
83
+ if (!ok(t.inner)) return `dangling type reference: ${at}.inner`;
84
+ break;
85
+ case "record":
86
+ case "variant": {
87
+ if (!Array.isArray(t.fields)) return `${at}: a ${t.kind} needs fields`;
88
+ const ids = new Set();
89
+ for (const f of t.fields) {
90
+ if (!ok(f?.type)) return `dangling type reference: ${at}.fields`;
91
+ if (!Number.isInteger(f.id) || f.id < 0 || f.id > U32_MAX || ids.has(f.id)) return `${at}: field ids must be distinct u32 values`;
92
+ ids.add(f.id);
93
+ }
94
+ break;
95
+ }
96
+ case "func":
97
+ if (!refs(t.args) || !refs(t.results)) return `dangling type reference: ${at}.args or .results`;
98
+ if (!MODES.has(t.mode)) return `${at}: a func needs a mode (update, query, composite_query or oneway), got ${JSON.stringify(t.mode)}`;
99
+ if (t.mode === "oneway" && t.results.length > 0) return `${at}: a oneway func has no results`;
100
+ break;
101
+ case "service":
102
+ if (!Array.isArray(t.methods)) return `${at}: a service needs methods`;
103
+ const methodNames = new Set();
104
+ for (const m of t.methods) {
105
+ if (typeof m?.name !== "string" || m.name === "") return `${at}: every method needs a non-empty name`;
106
+ if (!Number.isInteger(m.id) || m.id < 0 || m.id > U32_MAX || m.id !== idlHash(m.name)) return `${at}: method ${JSON.stringify(m.name)} needs the id ${idlHash(m.name)} (the hash of its name), got ${JSON.stringify(m.id)}`;
107
+ if (methodNames.has(m.name)) return `${at}: method ${JSON.stringify(m.name)} is listed twice`;
108
+ methodNames.add(m.name);
109
+ if (!isKind(m.function, "func")) return `${at}: method ${JSON.stringify(m.name)} does not name a func type`;
110
+ }
111
+ break;
112
+ case "class":
113
+ if (!refs(t.init) || !isKind(t.service, "service")) return `${at}: a class needs init arguments and a service type`;
114
+ break;
115
+ }
116
+ }
117
+ // each opt is walked once: a chain already known to end in a non-opt is not walked again (linear in the graph)
118
+ const state = new Uint8Array(n); // 0 unseen, 1 on the chain being walked, 2 ends in a non-opt
119
+ for (const [i, t] of c.types.entries()) {
120
+ if (t.kind !== "opt" || state[i] !== 0) continue;
121
+ const chain = [];
122
+ let r = i;
123
+ for (; c.types[r].kind === "opt" && state[r] === 0; r = c.types[r].inner) {
124
+ state[r] = 1;
125
+ chain.push(r);
126
+ }
127
+ if (c.types[r].kind === "opt" && state[r] === 1) return `types[${i}]: an opt that contains only opts of itself`;
128
+ for (const q of chain) state[q] = 2;
129
+ }
130
+ // A class is valid only as the actor (contract-graph.md, validation checklist 5): no declaration or type refers to one.
131
+ const isClass = (r) => c.types[r].kind === "class";
132
+ if (c.declarations.some((d) => isClass(d.type))) return "a declaration names a class: a class is only the actor";
133
+ for (const [i, t] of c.types.entries()) {
134
+ if (childrenOf(t).some(isClass)) return `types[${i}] refers to a class: a class is only the actor`;
135
+ }
136
+ const a = c.actor;
137
+ if (a === null || typeof a !== "object") return "no actor: the envelope describes no service";
138
+ if (a.kind === "service" ? !isKind(a.service, "service") : a.kind === "class" ? !isKind(a.class, "class") : true) {
139
+ return "actor must name a service or a class type";
140
+ }
141
+ // Every node is reachable from the actor or a declaration (contract-graph.md, validation checklist 6): nothing the
142
+ // interface id does not cover rides along in an envelope this resolver accepts.
143
+ const reached = new Uint8Array(n);
144
+ const stack = [a.kind === "service" ? a.service : a.class, ...c.declarations.map((d) => d.type)];
145
+ for (const r of stack) reached[r] = 1;
146
+ while (stack.length > 0) {
147
+ for (const r of childrenOf(c.types[stack.pop()])) {
148
+ if (reached[r] === 0) {
149
+ reached[r] = 1;
150
+ stack.push(r);
151
+ }
152
+ }
153
+ }
154
+ const stray = reached.indexOf(0);
155
+ if (stray !== -1) return `types[${stray}] is reached from neither the actor nor a declaration`;
156
+ const named = new Set();
157
+ for (const [k, e] of names.entries()) {
158
+ if (!Array.isArray(e) || e.length !== 3) return `field-names entry ${k} is not [container, id, name]`;
159
+ const [container, id, name] = e;
160
+ const t = ok(container) ? c.types[container] : undefined;
161
+ if (t?.kind !== "record" && t?.kind !== "variant") return `field-names entry ${k} names no record or variant`;
162
+ if (typeof name !== "string" || !t.fields.some((f) => f.id === id) || idlHash(name) !== id) return `field-names entry ${k}: ${JSON.stringify(name)} is not the name of field ${id} of types[${container}]`;
163
+ // two names for one field (they can share a hash) would leave the name to the order of the entries
164
+ if (named.has(`${container}:${id}`)) return `field-names entry ${k} names field ${id} of types[${container}] a second time`;
165
+ named.add(`${container}:${id}`);
166
+ }
167
+ const twin = duplicateNodes(c.types);
168
+ if (twin) return `types[${twin[0]}] and types[${twin[1]}] are the same type: the graph is not interned as candid-core-canon-1 requires`;
169
+ // A field with no entry keeps its numeric label (`#n`, spec §3.2 rule 1): candid-core writes none for `1 : nat` or a tuple
170
+ // field, so a missing entry cannot be told from a numeric label and is not checked.
171
+ return null;
172
+ }
173
+
174
+ /**
175
+ * The references of a node, in a fixed order, reading only the properties its kind has (checked above): another
176
+ * property on the node, such as `fields` on a primitive, is ignored. Record and variant fields go by id, methods by name.
177
+ */
178
+ function childrenOf(t) {
179
+ switch (t.kind) {
180
+ case "opt": case "vec": return [t.inner];
181
+ case "record": case "variant": return [...t.fields].sort((a, b) => a.id - b.id).map((f) => f.type);
182
+ case "func": return [...t.args, ...t.results];
183
+ case "service": return [...t.methods].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)).map((m) => m.function);
184
+ case "class": return [...t.init, t.service];
185
+ default: return [];
186
+ }
187
+ }
188
+
189
+ /** What a node is apart from its references: two nodes with equal keys are equal types if their references are. */
190
+ function localKey(t) {
191
+ switch (t.kind) {
192
+ case "primitive": return JSON.stringify(["p", t.primitive]);
193
+ case "record": case "variant": return JSON.stringify([t.kind, t.fields.map((f) => f.id).sort((a, b) => a - b)]);
194
+ case "func": return JSON.stringify(["f", t.mode, t.args.length, t.results.length]);
195
+ case "service": return JSON.stringify(["s", t.methods.map((m) => m.name).sort()]);
196
+ case "class": return JSON.stringify(["c", t.init.length]);
197
+ default: return JSON.stringify([t.kind]);
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Two indexes holding structurally equal types (recursion included), or null. candid-core-canon-1 interns every type
203
+ * once, and the resolver relies on it: an alias reaches a method position only through an equal reference (profile
204
+ * spec 3.2 rule 7). Equal types are the states a DFA minimization merges, so this is Hopcroft's partition refinement
205
+ * (O(k n log n) for n nodes with at most k references each): start from blocks of equal `localKey`, and split a block whenever its nodes' j-th
206
+ * references fall in different blocks, always queueing the smaller half. Two nodes still sharing a block are equal.
207
+ */
208
+ function duplicateNodes(types) {
209
+ const n = types.length;
210
+ const succ = types.map(childrenOf);
211
+ const preds = types.map(() => new Map()); // preds[t].get(j): the nodes whose j-th reference is t
212
+ for (const [s, refs] of succ.entries()) {
213
+ for (const [j, t] of refs.entries()) {
214
+ const list = preds[t].get(j);
215
+ if (list) list.push(s);
216
+ else preds[t].set(j, [s]);
217
+ }
218
+ }
219
+ // blocks are segments [start, end) of `elems`; `where[i]` is i's index in `elems`
220
+ const elems = new Int32Array(n);
221
+ const where = new Int32Array(n);
222
+ const block = new Int32Array(n);
223
+ const start = [];
224
+ const end = [];
225
+ const groups = new Map();
226
+ for (const [i, t] of types.entries()) {
227
+ const k = localKey(t);
228
+ if (!groups.has(k)) groups.set(k, []);
229
+ groups.get(k).push(i);
230
+ }
231
+ let fill = 0;
232
+ for (const members of groups.values()) {
233
+ start.push(fill);
234
+ for (const i of members) {
235
+ block[i] = start.length - 1;
236
+ where[i] = fill;
237
+ elems[fill++] = i;
238
+ }
239
+ end.push(fill);
240
+ }
241
+ const queued = new Set();
242
+ const work = [];
243
+ const queue = (b) => {
244
+ for (let x = start[b]; x < end[b]; x++) {
245
+ for (const j of preds[elems[x]].keys()) {
246
+ const key = `${b},${j}`;
247
+ if (!queued.has(key)) {
248
+ queued.add(key);
249
+ work.push([b, j]);
250
+ }
251
+ }
252
+ }
253
+ };
254
+ for (let b = 0; b < start.length; b++) queue(b);
255
+ const marked = [];
256
+ while (work.length > 0) {
257
+ const [b, j] = work.pop();
258
+ queued.delete(`${b},${j}`);
259
+ const touched = [];
260
+ // the splitter's members are copied first: marking moves nodes inside their blocks, the splitter's own included
261
+ for (const t of elems.slice(start[b], end[b])) {
262
+ for (const s of preds[t].get(j) ?? []) {
263
+ // s has one j-th reference, so it is marked at most once per splitter; marked nodes move to their block's front
264
+ const y = block[s];
265
+ if (marked[y] === undefined || marked[y] === 0) {
266
+ marked[y] = 0;
267
+ touched.push(y);
268
+ }
269
+ const to = start[y] + marked[y]++;
270
+ const other = elems[to];
271
+ elems[where[s]] = other;
272
+ where[other] = where[s];
273
+ elems[to] = s;
274
+ where[s] = to;
275
+ }
276
+ }
277
+ for (const y of touched) {
278
+ const m = marked[y];
279
+ marked[y] = 0;
280
+ const size = end[y] - start[y];
281
+ if (m === size) continue;
282
+ // the smaller part becomes the new block z; y keeps the rest (and any of its splitters already queued)
283
+ const z = start.length;
284
+ if (m <= size - m) {
285
+ start.push(start[y]);
286
+ end.push(start[y] + m);
287
+ start[y] += m;
288
+ } else {
289
+ start.push(start[y] + m);
290
+ end.push(end[y]);
291
+ end[y] = start[y] + m;
292
+ }
293
+ for (let x = start[z]; x < end[z]; x++) block[elems[x]] = z;
294
+ queue(z);
295
+ }
296
+ }
297
+ for (let b = 0; b < start.length; b++) {
298
+ if (end[b] - start[b] > 1) {
299
+ const pair = [elems[start[b]], elems[start[b] + 1]].sort((p, q) => p - q);
300
+ return pair;
301
+ }
302
+ }
303
+ return null;
304
+ }