@request-seal/core 0.1.0 → 0.3.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.
Files changed (128) hide show
  1. package/CHANGELOG.md +49 -1
  2. package/README.md +342 -114
  3. package/dist/accept-signature.d.ts +11 -0
  4. package/dist/accept-signature.js +7 -0
  5. package/dist/adapters/fetch/index.d.ts +10 -0
  6. package/dist/adapters/fetch/index.js +21 -4
  7. package/dist/adapters/fetch/runtime.d.ts +1 -0
  8. package/dist/adapters/fetch/runtime.js +1 -0
  9. package/dist/adapters/node-http/index.d.ts +23 -0
  10. package/dist/adapters/node-http/index.js +28 -8
  11. package/dist/brand.d.ts +5 -0
  12. package/dist/brand.js +12 -0
  13. package/dist/bytes.d.ts +4 -0
  14. package/dist/bytes.js +5 -0
  15. package/dist/constants.d.ts +64 -4
  16. package/dist/constants.js +62 -2
  17. package/dist/context.d.ts +4 -0
  18. package/dist/context.js +3 -0
  19. package/dist/crypto/algorithm.d.ts +9 -0
  20. package/dist/crypto/algorithm.js +12 -0
  21. package/dist/crypto/index.d.ts +12 -2
  22. package/dist/crypto/index.js +34 -11
  23. package/dist/crypto/options.d.ts +3 -0
  24. package/dist/crypto/options.js +15 -0
  25. package/dist/crypto/primitive.d.ts +7 -3
  26. package/dist/crypto/primitive.js +36 -10
  27. package/dist/custody/content_key.d.ts +4 -0
  28. package/dist/custody/content_key.js +4 -2
  29. package/dist/custody/index.d.ts +2 -0
  30. package/dist/custody/index.js +5 -1
  31. package/dist/custody/local.d.ts +17 -1
  32. package/dist/custody/local.js +10 -5
  33. package/dist/custody/ssh-agent/index.d.ts +2 -0
  34. package/dist/custody/ssh-agent/index.js +11 -181
  35. package/dist/custody/ssh-agent/port.d.ts +19 -0
  36. package/dist/custody/ssh-agent/port.js +183 -0
  37. package/dist/digest.d.ts +22 -0
  38. package/dist/digest.js +15 -0
  39. package/dist/discovery/body.d.ts +19 -2
  40. package/dist/discovery/body.js +36 -11
  41. package/dist/discovery/cache.d.ts +21 -2
  42. package/dist/discovery/cache.js +105 -39
  43. package/dist/discovery/headers.d.ts +3 -0
  44. package/dist/discovery/headers.js +27 -1
  45. package/dist/discovery/index.d.ts +13 -1
  46. package/dist/discovery/index.js +31 -7
  47. package/dist/discovery/node-connection.d.ts +1 -1
  48. package/dist/discovery/node-connection.js +2 -2
  49. package/dist/discovery/node-trust.d.ts +2 -0
  50. package/dist/discovery/node-trust.js +9 -0
  51. package/dist/discovery/node.d.ts +9 -1
  52. package/dist/discovery/node.js +150 -26
  53. package/dist/discovery/proof.js +2 -2
  54. package/dist/discovery/snapshot.js +3 -0
  55. package/dist/discovery/source.d.ts +7 -0
  56. package/dist/discovery/source.js +5 -0
  57. package/dist/discovery/support.js +2 -3
  58. package/dist/errors/parity.d.ts +44 -3
  59. package/dist/errors/parity.js +45 -4
  60. package/dist/errors/types.d.ts +17 -3
  61. package/dist/errors/types.js +16 -2
  62. package/dist/index.d.ts +1 -0
  63. package/dist/index.js +1 -0
  64. package/dist/jose/header.d.ts +1 -0
  65. package/dist/jose/jwe.d.ts +9 -0
  66. package/dist/jose/jwe.js +17 -3
  67. package/dist/jose/jws.d.ts +11 -0
  68. package/dist/jose/jws.js +14 -2
  69. package/dist/jose/key-management.d.ts +11 -0
  70. package/dist/jose/key-management.js +13 -2
  71. package/dist/jose/nested.d.ts +6 -0
  72. package/dist/jose/nested.js +32 -2
  73. package/dist/json.d.ts +7 -0
  74. package/dist/key_handle.d.ts +12 -3
  75. package/dist/key_handle.js +32 -25
  76. package/dist/key_identity.d.ts +7 -0
  77. package/dist/key_identity.js +10 -2
  78. package/dist/message/index.d.ts +24 -0
  79. package/dist/message/index.js +22 -1
  80. package/dist/numbers.d.ts +6 -0
  81. package/dist/numbers.js +5 -0
  82. package/dist/policy.d.ts +27 -0
  83. package/dist/policy.js +42 -21
  84. package/dist/profile.d.ts +36 -0
  85. package/dist/profile.js +215 -0
  86. package/dist/public_key.d.ts +11 -2
  87. package/dist/public_key.js +31 -8
  88. package/dist/quorum/evaluation.d.ts +1 -1
  89. package/dist/quorum/evaluation.js +18 -4
  90. package/dist/quorum/verification.d.ts +2 -0
  91. package/dist/quorum/verification.js +1 -0
  92. package/dist/quorum.d.ts +9 -0
  93. package/dist/quorum.js +7 -1
  94. package/dist/replay/claim.d.ts +5 -0
  95. package/dist/replay/claim.js +9 -1
  96. package/dist/replay/index.d.ts +6 -0
  97. package/dist/replay/index.js +13 -1
  98. package/dist/replay/postgres.d.ts +3 -0
  99. package/dist/replay/postgres.js +7 -0
  100. package/dist/replay/receipt.d.ts +1 -0
  101. package/dist/replay/receipt.js +1 -0
  102. package/dist/replay/store.d.ts +1 -0
  103. package/dist/replay/store.js +1 -0
  104. package/dist/replay-port.d.ts +6 -1
  105. package/dist/result.d.ts +3 -0
  106. package/dist/result.js +2 -0
  107. package/dist/sign.d.ts +26 -0
  108. package/dist/sign.js +25 -3
  109. package/dist/signature-base/index.d.ts +2 -0
  110. package/dist/signature-base/index.js +5 -1
  111. package/dist/signature-fields.d.ts +5 -1
  112. package/dist/signature-fields.js +9 -1
  113. package/dist/structured-fields/index.d.ts +21 -0
  114. package/dist/structured-fields/index.js +14 -1
  115. package/dist/verification.d.ts +5 -1
  116. package/dist/verification.js +9 -6
  117. package/dist/verify-quorum.d.ts +1 -0
  118. package/dist/verify-quorum.js +30 -8
  119. package/dist/verify.d.ts +23 -0
  120. package/dist/verify.js +46 -14
  121. package/dist/web-bot-auth/index.d.ts +3 -0
  122. package/dist/web-bot-auth/index.js +3 -0
  123. package/dist/web-bot-auth/policy.d.ts +8 -0
  124. package/dist/web-bot-auth/policy.js +8 -4
  125. package/dist/web-bot-auth/verification.d.ts +8 -0
  126. package/dist/web-bot-auth/verification.js +4 -2
  127. package/dist/web-bot-auth/verify.js +2 -2
  128. package/package.json +29 -6
package/dist/policy.js CHANGED
@@ -53,9 +53,33 @@ export function fieldSchemas(input) {
53
53
  return err(bounded(e, "invalid_policy", "input"));
54
54
  }
55
55
  }
56
+ /** Check the private brand of a verification policy. */
56
57
  export function isPolicy(value) {
57
58
  return typeof value === "object" && value !== null && policies.has(value);
58
59
  }
60
+ /** Internal shared freshness boundary for policies and extension preflight. */
61
+ export function normalizeFreshness(input) {
62
+ if (input === "not_evaluated")
63
+ return input;
64
+ const f = snapshot(input, [
65
+ "clock",
66
+ "maxAge",
67
+ "skew",
68
+ "requireExpires",
69
+ ]);
70
+ ensure(Object.keys(f).length === 4 &&
71
+ typeof f.clock === "function" &&
72
+ (f.maxAge === null || integer(f.maxAge, 1, CLOCK_MAX_SECONDS)) &&
73
+ integer(f.skew, 0, FRESHNESS_MAX_SKEW_SECONDS) &&
74
+ typeof f.requireExpires === "boolean", "invalid_policy", "input");
75
+ return Object.freeze({
76
+ clock: f.clock,
77
+ maxAge: f.maxAge,
78
+ skew: f.skew,
79
+ requireExpires: f.requireExpires,
80
+ });
81
+ }
82
+ /** Snapshot, validate, and brand an explicit verification policy. */
59
83
  export function createPolicy(input) {
60
84
  try {
61
85
  const a = snapshot(input, [
@@ -80,26 +104,7 @@ export function createPolicy(input) {
80
104
  const algorithms = algorithmList(a.algorithms), components = inner(a.components);
81
105
  ensure(components.ok && components.value.parameters.length === 0, "invalid_policy", "input");
82
106
  ensure(typeof a.keyResolver === "function", "invalid_policy", "input");
83
- let freshness = "not_evaluated";
84
- if (a.freshness !== "not_evaluated") {
85
- const f = snapshot(a.freshness, [
86
- "clock",
87
- "maxAge",
88
- "skew",
89
- "requireExpires",
90
- ]);
91
- ensure(Object.keys(f).length === 4 &&
92
- typeof f.clock === "function" &&
93
- (f.maxAge === null || integer(f.maxAge, 1, CLOCK_MAX_SECONDS)) &&
94
- integer(f.skew, 0, FRESHNESS_MAX_SKEW_SECONDS) &&
95
- typeof f.requireExpires === "boolean", "invalid_policy", "input");
96
- freshness = Object.freeze({
97
- clock: f.clock,
98
- maxAge: f.maxAge,
99
- skew: f.skew,
100
- requireExpires: f.requireExpires,
101
- });
102
- }
107
+ const freshness = normalizeFreshness(a.freshness);
103
108
  let content = "not_required";
104
109
  if (a.content !== "not_required") {
105
110
  const c = snapshot(a.content, ["kind", "algorithms", "section"]);
@@ -171,4 +176,20 @@ export function createPolicy(input) {
171
176
  return err(bounded(e, "invalid_policy", "input"));
172
177
  }
173
178
  }
174
- export const Policy = Object.freeze({ create: createPolicy });
179
+ /** Verification policy construction. */
180
+ export const Policy = Object.freeze({
181
+ /**
182
+ * Snapshot, validate, and brand an explicit verification policy.
183
+ *
184
+ * Six choices are required and none is defaulted: `algorithms`, `components`,
185
+ * `keyResolver`, `freshness`, `content`, and `replay`. Optional members are
186
+ * `extraComponents` (`"allow"` or `"reject"`), `maxSignatures` (1 through 64, default 16),
187
+ * and `fieldSchemas`.
188
+ *
189
+ * @returns `ok(policy)`, or `core` `input.invalid_policy` when any choice is missing,
190
+ * unknown, or malformed.
191
+ * @see [Signing and verifying guide](https://github.com/baselabs/request-seal-typescript/blob/main/docs/guides/signing-and-verifying.md#verify) and
192
+ * [reference](https://github.com/baselabs/request-seal-typescript/blob/main/docs/reference/sign-and-verify.md).
193
+ */
194
+ create: createPolicy
195
+ });
@@ -0,0 +1,36 @@
1
+ import { type Result } from "./result.ts";
2
+ import { type InnerList, type Item } from "./structured-fields/index.ts";
3
+ import type { Verification, FreshnessFacts } from "./verification.ts";
4
+ /** Descriptive extension selection; source-specific enforcement belongs to the extension. */
5
+ export type ProfileMetadataValue = string | boolean | null | number;
6
+ /** A namespaced extension profile name and bounded public metadata. */
7
+ export type ProfileStamp<MetadataKey extends string = never> = Readonly<{
8
+ name: readonly [packageName: string, kind: string];
9
+ }> & Readonly<Record<Exclude<MetadataKey, "name">, ProfileMetadataValue>>;
10
+ /** Paired parsed Signature-Input and Signature dictionaries. */
11
+ export type SignatureDictionaries = readonly [
12
+ readonly (readonly [string, InnerList])[],
13
+ Readonly<Record<string, Item>>
14
+ ];
15
+ /** The predicate checks signature semantics, not wire grammar or key trust. */
16
+ declare function validSignatureInput(input: unknown): boolean;
17
+ /** Parse paired Signature-Input and Signature dictionaries within the member limit. */
18
+ declare function dictionaries(message: unknown, max: number): Result<SignatureDictionaries>;
19
+ /** Validate covered components and signature freshness before verification. */
20
+ declare function preflight(input: unknown, components: unknown, extra: "allow" | "reject", freshness: unknown): Promise<Result<FreshnessFacts | "not_evaluated">>;
21
+ /** Verify a selected signature label under an explicit extension profile stamp. */
22
+ declare function verifyLabel<K extends string>(message: unknown, policy: unknown, selectedLabel: unknown, input: unknown, signature: unknown, stamp: ProfileStamp<K>): Promise<Result<Verification>>;
23
+ /** Verify a selected signature label under an explicit extension profile stamp. */
24
+ declare function verifyLabel(message: unknown, policy: unknown, selectedLabel: unknown, input: unknown, signature: unknown, stamp: unknown): Promise<Result<Verification>>;
25
+ /** Unstable extension boundary. Pin the package exactly and keep profile names constant. */
26
+ export declare const Profile: Readonly<{
27
+ /** Check the brand and shape of a signature-input inner list. */
28
+ validSignatureInput: typeof validSignatureInput;
29
+ /** Parse paired Signature-Input and Signature dictionaries within the member limit. */
30
+ dictionaries: typeof dictionaries;
31
+ /** Validate covered components and signature freshness before verification. */
32
+ preflight: typeof preflight;
33
+ /** Verify a selected signature label under an explicit extension profile stamp. */
34
+ verifyLabel: typeof verifyLabel;
35
+ }>;
36
+ export {};
@@ -0,0 +1,215 @@
1
+ import { encodeUtf8 } from "./runtime.js";
2
+ import { isMessage, Message } from "./message/index.js";
3
+ import { isPolicy, normalizeFreshness } from "./policy.js";
4
+ import { withDeadline, releaseContext } from "./context.js";
5
+ import { SF_MAX_INTEGER, SIGNATURE_MAX_BYTES, STRUCTURED_FIELDS_LIMITS, PROFILE_MAX_ATOM_CODE_POINTS, PROFILE_MAX_MEMBERS, PROFILE_MAX_VALUE_BYTES } from "./constants.js";
6
+ import { dictionaries as parseDictionaries, validInner, inner, label, ensure } from "./signature-fields.js";
7
+ import { verifyLabel as authenticateLabel, authenticationPreflight } from "./verify.js";
8
+ import { coreError, isRequestSealError } from "./errors/types.js";
9
+ import { ok, err } from "./result.js";
10
+ import { decimalSource } from "./structured-fields/index.js";
11
+ const reserved = new Set(["request_seal", "request-seal", "rfc9421", "web_bot_auth", "web-bot-auth"]);
12
+ function stampString(value, max) {
13
+ if (value.length > max * 2)
14
+ return false;
15
+ let count = 0;
16
+ for (const point of value) {
17
+ const code = point.codePointAt(0);
18
+ if (code >= 0xd800 && code <= 0xdfff || ++count > max)
19
+ return false;
20
+ }
21
+ return true;
22
+ }
23
+ function plain(value) {
24
+ if (value === null || typeof value !== "object" || Array.isArray(value))
25
+ return false;
26
+ const prototype = Object.getPrototypeOf(value);
27
+ return prototype === Object.prototype || prototype === null;
28
+ }
29
+ /** Read the dynamic metadata vocabulary once, without retaining caller objects. */
30
+ function snapshotStamp(value) {
31
+ ensure(plain(value), "invalid_profile", "input");
32
+ const keys = Reflect.ownKeys(value);
33
+ ensure(keys.length <= PROFILE_MAX_MEMBERS && keys.includes("name") && keys.every(k => typeof k === "string" && stampString(k, PROFILE_MAX_ATOM_CODE_POINTS)), "invalid_profile", "input");
34
+ const ordered = ["name", ...keys.filter(k => k !== "name").sort()];
35
+ const owned = Object.create(null);
36
+ for (const key of ordered)
37
+ owned[key] = value[key];
38
+ const name = owned.name;
39
+ ensure(Array.isArray(name), "invalid_profile", "input");
40
+ const nameKeys = Reflect.ownKeys(name);
41
+ ensure(nameKeys.length === 3 && nameKeys.includes("length") && nameKeys.includes("0") && nameKeys.includes("1") && name.length === 2, "invalid_profile", "input");
42
+ const packageName = name[0], kind = name[1];
43
+ ensure(typeof packageName === "string" && stampString(packageName, PROFILE_MAX_ATOM_CODE_POINTS) && typeof kind === "string" && stampString(kind, PROFILE_MAX_ATOM_CODE_POINTS) && !reserved.has(packageName), "invalid_profile", "input");
44
+ for (const key of keys) {
45
+ if (key === "name")
46
+ continue;
47
+ const v = owned[key];
48
+ ensure(v === null || typeof v === "boolean" ||
49
+ (typeof v === "number" && Number.isSafeInteger(v) && Math.abs(v) <= SF_MAX_INTEGER) ||
50
+ (typeof v === "string" && stampString(v, PROFILE_MAX_VALUE_BYTES) && encodeUtf8(v).length <= PROFILE_MAX_VALUE_BYTES), "invalid_profile", "input");
51
+ if (typeof v === "number" && Object.is(v, -0))
52
+ owned[key] = 0;
53
+ }
54
+ owned.name = Object.freeze([packageName, kind]);
55
+ // Sorted insertion gives equal stamps one key order; JavaScript lists array-index keys first.
56
+ return Object.freeze(owned);
57
+ }
58
+ function protectedError(e) {
59
+ return isRequestSealError(e) ? e : coreError("invalid_policy", "input");
60
+ }
61
+ /** The predicate checks signature semantics, not wire grammar or key trust. */
62
+ function validSignatureInput(input) {
63
+ try {
64
+ return validInner(input);
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
70
+ /** Parse paired Signature-Input and Signature dictionaries within the member limit. */
71
+ function dictionaries(message, max) {
72
+ try {
73
+ ensure(isMessage(message) && Number.isSafeInteger(max) && max >= 1 && max <= STRUCTURED_FIELDS_LIMITS.maxMembers, "invalid_policy", "input");
74
+ const [inputs, signatures] = paired(message, max);
75
+ const entries = Object.freeze([...inputs].map(([k, v]) => Object.freeze([k, v])));
76
+ const lookup = Object.create(null);
77
+ for (const [key, value] of signatures)
78
+ lookup[key] = value;
79
+ return ok(Object.freeze([entries, Object.freeze(lookup)]));
80
+ }
81
+ catch (e) {
82
+ return err(protectedError(e));
83
+ }
84
+ }
85
+ function paired(message, max) {
86
+ const values = parseDictionaries(message, max);
87
+ const [inputs, signatures] = values;
88
+ ensure(inputs.size === signatures.size && [...inputs.keys()].every(k => signatures.has(k)), "label_mismatch", "fields");
89
+ return values;
90
+ }
91
+ /** Validate covered components and signature freshness before verification. */
92
+ async function preflight(input, components, extra, freshness) {
93
+ try {
94
+ const required = inner(components);
95
+ if (!required.ok)
96
+ throw required.error.reason === "limit" ? coreError("limit", "input") : coreError("invalid_signature_input", "fields");
97
+ ensure(extra === "allow" || extra === "reject", "invalid_policy", "input");
98
+ const context = withDeadline();
99
+ if (!context.ok)
100
+ throw context.error;
101
+ try {
102
+ return ok(await authenticationPreflight(input, required.value, extra, () => normalizeFreshness(freshness), context.value));
103
+ }
104
+ finally {
105
+ releaseContext(context.value);
106
+ }
107
+ }
108
+ catch (e) {
109
+ return err(protectedError(e));
110
+ }
111
+ }
112
+ /** Expected parser structure bounds traversal; unknown keys cannot hide extra values.
113
+ * Ordered parameters/components and decoded byte equality mirror the reference values.
114
+ * Each caller property is read once, independent of object insertion order or identity.
115
+ */
116
+ function equalParsed(actual, expected) {
117
+ if (expected === null || typeof expected !== "object")
118
+ return actual === expected;
119
+ if (expected instanceof Uint8Array) {
120
+ if (!(actual instanceof Uint8Array) || actual.length !== expected.length)
121
+ return false;
122
+ const bytes = new Uint8Array(actual);
123
+ return bytes.every((b, i) => b === expected[i]);
124
+ }
125
+ if (Array.isArray(expected)) {
126
+ if (!Array.isArray(actual))
127
+ return false;
128
+ const keys = Reflect.ownKeys(actual);
129
+ if (actual.length !== expected.length || keys.length !== expected.length + 1 || !keys.includes("length"))
130
+ return false;
131
+ for (let i = 0; i < expected.length; i++)
132
+ if (!Object.hasOwn(actual, String(i)) || !equalParsed(actual[i], expected[i]))
133
+ return false;
134
+ return true;
135
+ }
136
+ if (!plain(actual))
137
+ return false;
138
+ const keys = Reflect.ownKeys(actual), expectedKeys = Object.keys(expected);
139
+ if (keys.length !== expectedKeys.length || !keys.every(k => typeof k === "string" && Object.hasOwn(expected, k)))
140
+ return false;
141
+ const fields = Object.create(null);
142
+ for (const key of expectedKeys)
143
+ fields[key] = actual[key];
144
+ const parsed = expected;
145
+ if (parsed.kind === "decimal") {
146
+ if (fields.kind !== "decimal")
147
+ return false;
148
+ const selected = decimalParts(actual, fields.value);
149
+ const authoritative = decimalParts(expected, parsed.value, decimalSource(actual) !== undefined);
150
+ return selected !== null && authoritative !== null && selected[0] === authoritative[0] && selected[1] === authoritative[1];
151
+ }
152
+ for (const key of expectedKeys)
153
+ if (!equalParsed(fields[key], parsed[key]))
154
+ return false;
155
+ return true;
156
+ }
157
+ /** Parsed decimals retain source coefficient/scale separately from numeric normalization.
158
+ * Leading zero and negative-zero spellings do not change the reference integer coefficient.
159
+ */
160
+ function decimalParts(node, value, useSource = true) {
161
+ if (!plain(value))
162
+ return null;
163
+ const keys = Reflect.ownKeys(value);
164
+ if (keys.length !== 2 || !keys.includes("scaled") || !keys.includes("scale"))
165
+ return null;
166
+ const scaled = value.scaled, scale = value.scale;
167
+ if (!(typeof scaled === "bigint" || typeof scaled === "number" && Number.isSafeInteger(scaled)) || typeof scale !== "number" || !Number.isSafeInteger(scale) || scale < 1 || scale > 3)
168
+ return null;
169
+ const source = useSource ? decimalSource(node) : undefined;
170
+ return source === undefined ? [BigInt(scaled), scale] : [BigInt(source.replace(".", "")), source.length - source.indexOf(".") - 1];
171
+ }
172
+ function signatureSnapshot(value) {
173
+ ensure(plain(value), "invalid_signature_field", "fields");
174
+ const keys = Reflect.ownKeys(value);
175
+ ensure(keys.length === 3 && keys.every(k => typeof k === "string" && ["kind", "value", "parameters"].includes(k)), "invalid_signature_field", "fields");
176
+ const kind = value.kind, bare = value.value, parameters = value.parameters;
177
+ ensure(kind === "item" && plain(bare), "invalid_signature_field", "fields");
178
+ const bareKeys = Reflect.ownKeys(bare);
179
+ ensure(bareKeys.length === 2 && bareKeys.includes("kind") && bareKeys.includes("value"), "invalid_signature_field", "fields");
180
+ const bareKind = bare.kind, bytes = bare.value;
181
+ ensure(bareKind === "bytes" && bytes instanceof Uint8Array && bytes.length >= 1 && bytes.length <= SIGNATURE_MAX_BYTES, "invalid_signature_field", "fields");
182
+ return { kind: "item", value: { kind: "bytes", value: new Uint8Array(bytes) }, parameters: parameters };
183
+ }
184
+ /** Verify a selected signature label under an explicit extension profile stamp. */
185
+ async function verifyLabel(message, policy, selectedLabel, input, signature, stamp) {
186
+ try {
187
+ const profile = snapshotStamp(stamp);
188
+ ensure(isPolicy(policy), "invalid_policy", "input");
189
+ ensure(label(selectedLabel), "invalid_options", "input");
190
+ ensure(isMessage(message), "invalid_message", "input");
191
+ const validated = Message.create(message);
192
+ ensure(validated.ok, "invalid_message", "input");
193
+ const selectedSignature = signatureSnapshot(signature);
194
+ const [inputs, signatures] = paired(validated.value, policy.maxSignatures);
195
+ ensure(inputs.has(selectedLabel), "unknown_label", "fields");
196
+ const selectedInput = inputs.get(selectedLabel);
197
+ ensure(equalParsed(input, selectedInput), "invalid_signature_input", "fields");
198
+ ensure(equalParsed(selectedSignature, signatures.get(selectedLabel)), "invalid_signature_field", "fields");
199
+ return await authenticateLabel(validated.value, policy, { label: selectedLabel }, undefined, { input: selectedInput, signature: signatures.get(selectedLabel), baseCache: new Map() }, profile);
200
+ }
201
+ catch (e) {
202
+ return err(protectedError(e));
203
+ }
204
+ }
205
+ /** Unstable extension boundary. Pin the package exactly and keep profile names constant. */
206
+ export const Profile = Object.freeze({
207
+ /** Check the brand and shape of a signature-input inner list. */
208
+ validSignatureInput,
209
+ /** Parse paired Signature-Input and Signature dictionaries within the member limit. */
210
+ dictionaries,
211
+ /** Validate covered components and signature freshness before verification. */
212
+ preflight,
213
+ /** Verify a selected signature label under an explicit extension profile stamp. */
214
+ verifyLabel
215
+ });
@@ -1,23 +1,32 @@
1
1
  import { type Result } from './result.ts';
2
2
  import { type Jwk } from './custody/jwk.ts';
3
+ /** Check the private brand of a public key. */
3
4
  export declare function isPublicKey(value: unknown): value is PublicKey;
4
5
  /** Public components only. Metadata restrictions survive every use and export. */
5
6
  export declare class PublicKey {
6
7
  #private;
7
8
  private constructor();
9
+ /** Import public JWK, PEM, or SPKI material without discarding metadata restrictions. */
8
10
  static import(material: unknown, format?: 'jwk' | 'pem' | 'spki' | 'der'): Promise<Result<PublicKey>>;
11
+ /** Export a defensive copy of the public JWK and its metadata restrictions. */
9
12
  jwk(): Result<Jwk>;
13
+ /** Export public SPKI bytes when metadata restrictions permit a lossless export. */
10
14
  spki(): Result<Uint8Array<ArrayBuffer>>;
15
+ /** Export public SPKI PEM when metadata restrictions permit a lossless export. */
11
16
  pem(): Result<string>;
17
+ /** Compute the public key thumbprint defined by RFC 7638. */
12
18
  thumbprint(): Promise<Result<string>>;
13
19
  /** Internal equivalence ignores PSS-only type and JWK metadata. */
14
20
  identityBytes(): Result<Uint8Array<ArrayBuffer>>;
21
+ /** Check compatibility and metadata permission for the selected algorithm. */
15
22
  bind(algorithm: unknown): Result<true>;
16
- verify(algorithm: unknown, signature: unknown, data: unknown): Promise<Result<true>>;
23
+ /** Verify a signature with bounded input and preserved key metadata restrictions. */
24
+ verify(algorithm: unknown, signature: unknown, data: unknown, options?: unknown): Promise<Result<true>>;
25
+ /** Return a bounded public inspection view without retained cryptographic bytes. */
17
26
  toJSON(): Readonly<{
18
27
  kind: string;
19
28
  pssOnly: boolean;
20
29
  }>;
21
30
  }
22
- export declare function verifyPublicKey(key: PublicKey, algorithm: unknown, signature: unknown, data: unknown): Promise<Result<true>>;
31
+ export declare function verifyPublicKey(key: PublicKey, algorithm: unknown, signature: unknown, data: unknown, maximum?: number): Promise<Result<true>>;
23
32
  export declare function bindPublicKey(key: PublicKey, algorithm: unknown): Result<true>;
@@ -1,14 +1,19 @@
1
+ import { denoInspection } from './brand.js';
2
+ import { byteLimit } from './crypto/options.js';
3
+ import { CRYPTO_DEFAULT_MAX_BYTES } from './constants.js';
1
4
  import { encodeUtf8 } from './runtime.js';
2
5
  import { ok, err } from './result.js';
3
6
  import { cryptoError } from './errors/types.js';
4
7
  import { encodeBase64Url } from './bytes.js';
5
8
  import { resolveAlgorithm, importParameters, } from './crypto/algorithm.js';
6
9
  import * as subtle from './crypto/subtle.js';
7
- import { verifyKey } from './crypto/primitive.js';
10
+ import { boundedData, verifyKey } from './crypto/primitive.js';
8
11
  import { decodeKey, publicSpki, writePem } from './custody/der.js';
9
12
  import { snapshotJwk, publicMembers, compatible, permitted, } from './custody/jwk.js';
13
+ const verifiers = new WeakMap();
10
14
  const branded = new WeakSet();
11
15
  const token = Object.freeze({});
16
+ /** Check the private brand of a public key. */
12
17
  export function isPublicKey(value) {
13
18
  return typeof value === 'object' && value !== null && branded.has(value);
14
19
  }
@@ -23,9 +28,11 @@ export class PublicKey {
23
28
  this.#jwk = jwk;
24
29
  this.#pssOnly = pssOnly;
25
30
  this.#spki = new Uint8Array(spki);
31
+ verifiers.set(this, this.#verify.bind(this));
26
32
  branded.add(this);
27
33
  Object.freeze(this);
28
34
  }
35
+ /** Import public JWK, PEM, or SPKI material without discarding metadata restrictions. */
29
36
  static async import(material, format = 'jwk') {
30
37
  try {
31
38
  let jwk, pssOnly = false;
@@ -76,16 +83,19 @@ export class PublicKey {
76
83
  return err(cryptoError('invalid_key'));
77
84
  }
78
85
  }
86
+ /** Export a defensive copy of the public JWK and its metadata restrictions. */
79
87
  jwk() {
80
88
  return this.#pssOnly
81
89
  ? err(cryptoError('unsupported_format'))
82
90
  : ok(this.#jwk);
83
91
  }
92
+ /** Export public SPKI bytes when metadata restrictions permit a lossless export. */
84
93
  spki() {
85
94
  return ['alg', 'use', 'key_ops'].some((k) => Object.hasOwn(this.#jwk, k))
86
95
  ? err(cryptoError('unsupported_format'))
87
96
  : ok(new Uint8Array(this.#spki));
88
97
  }
98
+ /** Export public SPKI PEM when metadata restrictions permit a lossless export. */
89
99
  pem() {
90
100
  const r = this.spki();
91
101
  if (!r.ok)
@@ -93,6 +103,7 @@ export class PublicKey {
93
103
  const p = writePem('PUBLIC KEY', r.value);
94
104
  return p.ok ? p : err(cryptoError('invalid_key'));
95
105
  }
106
+ /** Compute the public key thumbprint defined by RFC 7638. */
96
107
  async thumbprint() {
97
108
  try {
98
109
  return ok(encodeBase64Url(new Uint8Array(await subtle.digest('SHA-256', encodeUtf8(JSON.stringify(publicMembers(this.#jwk)))))));
@@ -105,6 +116,7 @@ export class PublicKey {
105
116
  identityBytes() {
106
117
  return ok(encodeUtf8(JSON.stringify(publicMembers(this.#jwk))));
107
118
  }
119
+ /** Check compatibility and metadata permission for the selected algorithm. */
108
120
  bind(algorithm) {
109
121
  const resolved = resolveAlgorithm(algorithm, true);
110
122
  if (!resolved.ok)
@@ -118,38 +130,49 @@ export class PublicKey {
118
130
  ? ok(true)
119
131
  : err(cryptoError('key_mismatch'));
120
132
  }
121
- async verify(algorithm, signature, data) {
133
+ /** Verify a signature with bounded input and preserved key metadata restrictions. */
134
+ async verify(algorithm, signature, data, options = {}) {
135
+ const maximum = byteLimit(options);
136
+ if (!maximum.ok)
137
+ return maximum;
138
+ return this.#verify(algorithm, signature, data, maximum.value);
139
+ }
140
+ async #verify(algorithm, signature, data, maximum) {
122
141
  const resolved = resolveAlgorithm(algorithm);
123
142
  if (!resolved.ok)
124
143
  return resolved;
125
144
  const spec = resolved.value;
145
+ // Same order as Crypto.verify: options, algorithm, data bound, then key compatibility.
146
+ if (!boundedData(data, maximum))
147
+ return err(cryptoError('invalid_data'));
126
148
  try {
127
149
  if (!compatible(this.#jwk, spec, this.#pssOnly) ||
128
150
  !permitted(this.#jwk, spec, 'verify'))
129
151
  return err(cryptoError('key_mismatch'));
130
152
  const key = await subtle.importKey('jwk', publicMembers(this.#jwk), importParameters(spec), false, ['verify']);
131
- return await verifyKey(spec, key, signature, data);
153
+ return await verifyKey(spec, key, signature, data, maximum);
132
154
  }
133
155
  catch {
134
156
  return err(cryptoError('invalid_key'));
135
157
  }
136
158
  }
159
+ /** Return a bounded public inspection view without retained cryptographic bytes. */
137
160
  toJSON() {
138
161
  return Object.freeze({ kind: 'PublicKey', pssOnly: this.#pssOnly });
139
162
  }
140
163
  [Symbol.for('nodejs.util.inspect.custom')]() {
141
164
  return this.toJSON();
142
165
  }
143
- [Symbol.for('Deno.customInspect')]() {
144
- return this.toJSON();
166
+ [Symbol.for('Deno.customInspect')](render) {
167
+ return denoInspection(render, this.toJSON());
145
168
  }
146
169
  }
147
170
  Object.freeze(PublicKey.prototype);
148
171
  Object.freeze(PublicKey);
149
- const publicVerify = Function.prototype.call.bind(PublicKey.prototype.verify);
150
172
  const publicBind = Function.prototype.call.bind(PublicKey.prototype.bind);
151
- export function verifyPublicKey(key, algorithm, signature, data) {
152
- return publicVerify(key, algorithm, signature, data);
173
+ export function verifyPublicKey(key, algorithm, signature, data, maximum = CRYPTO_DEFAULT_MAX_BYTES) {
174
+ const verify = verifiers.get(key);
175
+ return verify ? verify(algorithm, signature, data, maximum) : Promise.resolve(err(cryptoError('invalid_key')));
153
176
  }
154
177
  export function bindPublicKey(key, algorithm) {
155
178
  return publicBind(key, algorithm);
@@ -16,7 +16,7 @@ export type Assignment = Readonly<{
16
16
  pool: readonly Candidate[];
17
17
  count: number;
18
18
  nodes: number;
19
- exhausted: boolean;
19
+ exhausted: false | "nodes" | "deadline";
20
20
  }>;
21
21
  /** Evaluation yields after about 10 ms of search work, preserving reference accounting. */
22
22
  export declare function evaluateQuorum(records: readonly Candidate[], q: Quorum, requests: AcceptSignature, context?: Context): Promise<Result<Assignment>>;
@@ -249,13 +249,19 @@ function* evaluate(records, q, requests, context) {
249
249
  if (exhausted)
250
250
  return;
251
251
  if (nodes >= QUORUM_MAX_NODES) {
252
- exhausted = true;
252
+ exhausted = "nodes";
253
253
  return;
254
254
  }
255
255
  nodes++;
256
256
  if (now() - sliceStart >= 10) {
257
257
  yield;
258
- tick();
258
+ // The caller's wall-clock bound marks deadline exhaustion like the
259
+ // node bound: best-so-far acceptance is preserved, and only a
260
+ // rejection surfaces it (retryable, unlike the node limit).
261
+ if (context && remainingMs(context) <= 0) {
262
+ exhausted = "deadline";
263
+ return;
264
+ }
259
265
  sliceStart = now();
260
266
  }
261
267
  if (i === slots.length) {
@@ -351,13 +357,21 @@ function* evaluate(records, q, requests, context) {
351
357
  }
352
358
  }
353
359
  yield* search(0, new Map(), new Map(), new Map());
354
- if (exhausted && (chosen === null || !modeMet(chosen, q)))
360
+ if (exhausted !== false && (chosen === null || !modeMet(chosen, q))) {
361
+ if (exhausted === "deadline")
362
+ fail("deadline", "quorum");
355
363
  fail("limit", "input");
364
+ }
356
365
  ensure(chosen !== null, "binding_unsatisfied", "quorum");
357
366
  }
358
367
  ensure(holds(new Map(chosen.map(r => [r.slot.id, r])), q.bindings), "binding_unsatisfied", "quorum");
359
368
  ensure(modeMet(chosen, q), "quorum_not_met", "quorum");
360
- tick();
369
+ // A deadline-exhausted acceptance is the whole point of the bound: the
370
+ // wall clock already passed, and the satisfying assignment still returns.
371
+ // (The search closure mutates the marker, which narrowing cannot see.)
372
+ const exhaustedNow = exhausted;
373
+ if (exhaustedNow !== "deadline")
374
+ tick();
361
375
  return ok(Object.freeze({
362
376
  chosen: Object.freeze(chosen),
363
377
  pool: Object.freeze(pool),
@@ -2,6 +2,7 @@ import { type Result } from "../result.ts";
2
2
  import { type Verification } from "../verification.ts";
3
3
  import type { QuorumMode, QuorumUnit, QuorumBinding } from "../quorum.ts";
4
4
  import type { Layer, Reason } from "../errors/parity.ts";
5
+ /** Branded per-slot verification and quorum evaluation facts. */
5
6
  export type QuorumVerification = Readonly<{
6
7
  profile: Readonly<{
7
8
  name: "rfc9421";
@@ -31,6 +32,7 @@ export type QuorumVerification = Readonly<{
31
32
  principal: "unattributed";
32
33
  authorization: "not_evaluated";
33
34
  }>;
35
+ /** Check the private brand of a quorum verification result. */
34
36
  export declare function isQuorumVerification(v: unknown): v is QuorumVerification;
35
37
  export declare function completeQuorum(value: QuorumVerification): Result<QuorumVerification>;
36
38
  export declare function quorumFacts(value: unknown): Result<unknown>;
@@ -2,6 +2,7 @@ import { ok, err } from "../result.js";
2
2
  import { coreError } from "../errors/types.js";
3
3
  import { verificationFacts } from "../verification.js";
4
4
  const results = new WeakSet();
5
+ /** Check the private brand of a quorum verification result. */
5
6
  export function isQuorumVerification(v) {
6
7
  return typeof v === "object" && v !== null && results.has(v);
7
8
  }
package/dist/quorum.d.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  import { type Policy } from "./policy.ts";
2
2
  import { type Result } from "./result.ts";
3
+ /** Require all, any, or a threshold number of successful signers. */
3
4
  export type QuorumMode = "all" | "any" | Readonly<{
4
5
  threshold: number;
5
6
  }>;
7
+ /** Count quorum satisfaction by keys, principals, or roles. */
6
8
  export type QuorumUnit = "keys" | "principals" | "roles";
9
+ /** A named quorum slot with a policy and optional signature and principal selectors. */
7
10
  export type SignerSlot = Readonly<{
8
11
  id: string;
9
12
  policy: Policy;
@@ -13,6 +16,7 @@ export type SignerSlot = Readonly<{
13
16
  principal: string | null;
14
17
  role: string | null;
15
18
  }>;
19
+ /** A same-component or same-parameter constraint across quorum slots. */
16
20
  export type QuorumBinding = Readonly<{
17
21
  kind: "nested";
18
22
  outer: string;
@@ -22,6 +26,7 @@ export type QuorumBinding = Readonly<{
22
26
  name: "nonce" | "tag" | "created";
23
27
  slots: readonly string[];
24
28
  }>;
29
+ /** A branded policy for bounded multi-signature verification. */
25
30
  export type Quorum = Readonly<{
26
31
  mode: QuorumMode;
27
32
  unit: QuorumUnit;
@@ -32,8 +37,12 @@ export type Quorum = Readonly<{
32
37
  bindings: readonly QuorumBinding[];
33
38
  maxSignatures: number;
34
39
  }>;
40
+ /** Check the private brand of a quorum policy. */
35
41
  export declare function isQuorum(v: unknown): v is Quorum;
42
+ /** Snapshot, validate, and brand a quorum policy. */
36
43
  export declare function createQuorum(input: unknown): Result<Quorum>;
44
+ /** Quorum policy construction. */
37
45
  export declare const Quorum: Readonly<{
46
+ /** Snapshot, validate, and brand a bounded quorum policy. */
38
47
  create: typeof createQuorum;
39
48
  }>;
package/dist/quorum.js CHANGED
@@ -8,6 +8,7 @@ const quorums = new WeakSet();
8
8
  const text = (v) => typeof v === "string" &&
9
9
  encodeUtf8(v).length >= 1 &&
10
10
  encodeUtf8(v).length <= 256;
11
+ /** Check the private brand of a quorum policy. */
11
12
  export function isQuorum(v) {
12
13
  return typeof v === "object" && v !== null && quorums.has(v);
13
14
  }
@@ -16,6 +17,7 @@ function snapshot(v, keys) {
16
17
  ensure(r.ok, "invalid_quorum", "input");
17
18
  return r.value;
18
19
  }
20
+ /** Snapshot, validate, and brand a quorum policy. */
19
21
  export function createQuorum(input) {
20
22
  try {
21
23
  const a = snapshot(input, [
@@ -142,4 +144,8 @@ export function createQuorum(input) {
142
144
  return err(bounded(e, "invalid_quorum", "input"));
143
145
  }
144
146
  }
145
- export const Quorum = Object.freeze({ create: createQuorum });
147
+ /** Quorum policy construction. */
148
+ export const Quorum = Object.freeze({
149
+ /** Snapshot, validate, and brand a bounded quorum policy. */
150
+ create: createQuorum
151
+ });