@request-seal/core 0.2.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 (41) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/README.md +10 -9
  3. package/dist/adapters/node-http/index.d.ts +8 -1
  4. package/dist/adapters/node-http/index.js +21 -5
  5. package/dist/constants.d.ts +4 -4
  6. package/dist/constants.js +2 -2
  7. package/dist/crypto/algorithm.d.ts +4 -0
  8. package/dist/crypto/algorithm.js +12 -0
  9. package/dist/discovery/body.d.ts +12 -2
  10. package/dist/discovery/body.js +33 -11
  11. package/dist/discovery/cache.d.ts +13 -3
  12. package/dist/discovery/cache.js +103 -39
  13. package/dist/discovery/headers.d.ts +2 -0
  14. package/dist/discovery/headers.js +27 -1
  15. package/dist/discovery/index.d.ts +9 -2
  16. package/dist/discovery/index.js +22 -6
  17. package/dist/discovery/node.d.ts +6 -2
  18. package/dist/discovery/node.js +124 -18
  19. package/dist/discovery/proof.js +2 -2
  20. package/dist/discovery/snapshot.js +3 -0
  21. package/dist/errors/parity.d.ts +29 -3
  22. package/dist/errors/parity.js +32 -3
  23. package/dist/errors/types.d.ts +4 -2
  24. package/dist/errors/types.js +14 -2
  25. package/dist/jose/nested.js +23 -1
  26. package/dist/message/index.js +2 -1
  27. package/dist/policy.d.ts +13 -1
  28. package/dist/policy.js +13 -1
  29. package/dist/quorum/evaluation.d.ts +1 -1
  30. package/dist/quorum/evaluation.js +18 -4
  31. package/dist/replay/postgres.d.ts +2 -0
  32. package/dist/replay/postgres.js +6 -0
  33. package/dist/sign.d.ts +22 -1
  34. package/dist/sign.js +25 -4
  35. package/dist/signature-fields.d.ts +5 -1
  36. package/dist/signature-fields.js +9 -1
  37. package/dist/verify-quorum.js +29 -8
  38. package/dist/verify.d.ts +19 -1
  39. package/dist/verify.js +32 -5
  40. package/dist/web-bot-auth/verify.js +2 -2
  41. package/package.json +13 -1
@@ -1,5 +1,5 @@
1
1
  /** The immutable reference release used for the error vocabulary. */
2
- export declare const ELIXIR_ERROR_PIN = "0a6568aa246326617f760e94e21b359285c6ddfc";
2
+ export declare const ELIXIR_ERROR_PIN = "53b6690ccd5036d58b280d08cf0cacc6427262fe";
3
3
  declare const retryableRules: {
4
4
  readonly core: {
5
5
  readonly actor_unbound: {
@@ -38,6 +38,10 @@ declare const retryableRules: {
38
38
  readonly kind: "constant";
39
39
  readonly value: false;
40
40
  };
41
+ readonly deadline: {
42
+ readonly kind: "constant";
43
+ readonly value: true;
44
+ };
41
45
  readonly digest_mismatch: {
42
46
  readonly kind: "constant";
43
47
  readonly value: false;
@@ -66,6 +70,10 @@ declare const retryableRules: {
66
70
  readonly kind: "constant";
67
71
  readonly value: false;
68
72
  };
73
+ readonly internal: {
74
+ readonly kind: "constant";
75
+ readonly value: false;
76
+ };
69
77
  readonly invalid_accept_signature: {
70
78
  readonly kind: "constant";
71
79
  readonly value: false;
@@ -644,6 +652,10 @@ declare const retryableRules: {
644
652
  readonly kind: "constant";
645
653
  readonly value: false;
646
654
  };
655
+ readonly domain_not_found: {
656
+ readonly kind: "constant";
657
+ readonly value: false;
658
+ };
647
659
  readonly invalid_key_set: {
648
660
  readonly kind: "constant";
649
661
  readonly value: false;
@@ -674,7 +686,7 @@ declare const retryableRules: {
674
686
  };
675
687
  readonly resolution_failed: {
676
688
  readonly kind: "constant";
677
- readonly value: false;
689
+ readonly value: true;
678
690
  };
679
691
  readonly revoked_key: {
680
692
  readonly kind: "constant";
@@ -701,7 +713,7 @@ declare const retryableRules: {
701
713
  readonly minimum: 500;
702
714
  readonly maximum: 599;
703
715
  readonly path: "lib/request_seal/discovery.ex";
704
- readonly line: 301;
716
+ readonly line: 517;
705
717
  };
706
718
  readonly unknown_key: {
707
719
  readonly kind: "constant";
@@ -1119,6 +1131,11 @@ export declare const ERROR_PARITY: readonly Readonly<{
1119
1131
  readonly layer: "input";
1120
1132
  readonly reason: "unsupported_component";
1121
1133
  readonly rule_id: "core/input.unsupported_component";
1134
+ } | {
1135
+ readonly kind: "core";
1136
+ readonly layer: "internal";
1137
+ readonly reason: "internal";
1138
+ readonly rule_id: "core/internal.internal";
1122
1139
  } | {
1123
1140
  readonly kind: "core";
1124
1141
  readonly layer: "key";
@@ -1204,6 +1221,11 @@ export declare const ERROR_PARITY: readonly Readonly<{
1204
1221
  readonly layer: "quorum";
1205
1222
  readonly reason: "binding_unsatisfied";
1206
1223
  readonly rule_id: "core/quorum.binding_unsatisfied";
1224
+ } | {
1225
+ readonly kind: "core";
1226
+ readonly layer: "quorum";
1227
+ readonly reason: "deadline";
1228
+ readonly rule_id: "core/quorum.deadline";
1207
1229
  } | {
1208
1230
  readonly kind: "core";
1209
1231
  readonly layer: "quorum";
@@ -1614,6 +1636,10 @@ export declare const ERROR_PARITY: readonly Readonly<{
1614
1636
  readonly kind: "discovery";
1615
1637
  readonly reason: "directory_unsigned";
1616
1638
  readonly rule_id: "discovery/directory_unsigned";
1639
+ } | {
1640
+ readonly kind: "discovery";
1641
+ readonly reason: "domain_not_found";
1642
+ readonly rule_id: "discovery/domain_not_found";
1617
1643
  } | {
1618
1644
  readonly kind: "discovery";
1619
1645
  readonly reason: "invalid_key_set";
@@ -1,6 +1,6 @@
1
1
  // Derived from pinned Elixir constructors and call sites; regenerate with scripts/generate-parity.ts or scripts/render-parity.ts --update-table.
2
2
  /** The immutable reference release used for the error vocabulary. */
3
- export const ELIXIR_ERROR_PIN = '0a6568aa246326617f760e94e21b359285c6ddfc';
3
+ export const ELIXIR_ERROR_PIN = '53b6690ccd5036d58b280d08cf0cacc6427262fe';
4
4
  const entries = [
5
5
  {
6
6
  "kind": "core",
@@ -254,6 +254,12 @@ const entries = [
254
254
  "reason": "unsupported_component",
255
255
  "rule_id": "core/input.unsupported_component"
256
256
  },
257
+ {
258
+ "kind": "core",
259
+ "layer": "internal",
260
+ "reason": "internal",
261
+ "rule_id": "core/internal.internal"
262
+ },
257
263
  {
258
264
  "kind": "core",
259
265
  "layer": "key",
@@ -356,6 +362,12 @@ const entries = [
356
362
  "reason": "binding_unsatisfied",
357
363
  "rule_id": "core/quorum.binding_unsatisfied"
358
364
  },
365
+ {
366
+ "kind": "core",
367
+ "layer": "quorum",
368
+ "reason": "deadline",
369
+ "rule_id": "core/quorum.deadline"
370
+ },
359
371
  {
360
372
  "kind": "core",
361
373
  "layer": "quorum",
@@ -866,6 +878,11 @@ const entries = [
866
878
  "reason": "directory_unsigned",
867
879
  "rule_id": "discovery/directory_unsigned"
868
880
  },
881
+ {
882
+ "kind": "discovery",
883
+ "reason": "domain_not_found",
884
+ "rule_id": "discovery/domain_not_found"
885
+ },
869
886
  {
870
887
  "kind": "discovery",
871
888
  "reason": "invalid_key_set",
@@ -1245,6 +1262,10 @@ const retryableRules = {
1245
1262
  "kind": "constant",
1246
1263
  "value": false
1247
1264
  },
1265
+ "deadline": {
1266
+ "kind": "constant",
1267
+ "value": true
1268
+ },
1248
1269
  "digest_mismatch": {
1249
1270
  "kind": "constant",
1250
1271
  "value": false
@@ -1273,6 +1294,10 @@ const retryableRules = {
1273
1294
  "kind": "constant",
1274
1295
  "value": false
1275
1296
  },
1297
+ "internal": {
1298
+ "kind": "constant",
1299
+ "value": false
1300
+ },
1276
1301
  "invalid_accept_signature": {
1277
1302
  "kind": "constant",
1278
1303
  "value": false
@@ -1851,6 +1876,10 @@ const retryableRules = {
1851
1876
  "kind": "constant",
1852
1877
  "value": false
1853
1878
  },
1879
+ "domain_not_found": {
1880
+ "kind": "constant",
1881
+ "value": false
1882
+ },
1854
1883
  "invalid_key_set": {
1855
1884
  "kind": "constant",
1856
1885
  "value": false
@@ -1881,7 +1910,7 @@ const retryableRules = {
1881
1910
  },
1882
1911
  "resolution_failed": {
1883
1912
  "kind": "constant",
1884
- "value": false
1913
+ "value": true
1885
1914
  },
1886
1915
  "revoked_key": {
1887
1916
  "kind": "constant",
@@ -1908,7 +1937,7 @@ const retryableRules = {
1908
1937
  "minimum": 500,
1909
1938
  "maximum": 599,
1910
1939
  "path": "lib/request_seal/discovery.ex",
1911
- "line": 301
1940
+ "line": 517
1912
1941
  },
1913
1942
  "unknown_key": {
1914
1943
  "kind": "constant",
@@ -1,13 +1,13 @@
1
1
  import { type Reason, type Layer } from './parity.ts';
2
2
  /** The bounded detail vocabulary for core failures. */
3
- export type CoreDetail = Reason<'core'> | Reason<'signature_base'> | 'legacy_string' | 'invalid_dictionary' | 'duplicate_member' | 'invalid_member' | 'missing_member' | 'unsupported_type' | 'not_an_origin' | 'untrusted_agent' | 'source_mismatch' | 'revoked_key' | 'source_unavailable' | 'timeout';
3
+ export type CoreDetail = Reason<'core'> | Reason<'signature_base'> | 'legacy_string' | 'invalid_dictionary' | 'duplicate_member' | 'invalid_member' | 'missing_member' | 'unsupported_type' | 'not_an_origin' | 'untrusted_agent' | 'source_mismatch' | 'revoked_key' | 'source_unavailable' | 'timeout' | 'TypeError' | 'RangeError' | 'URIError' | 'EvalError' | 'ReferenceError' | 'SyntaxError' | 'Error' | 'throw';
4
4
  /** A core failure with layer, correlation, and optional bounded source. */
5
5
  export type CoreError = Readonly<{
6
6
  kind: 'core';
7
7
  reason: Reason<'core'>;
8
8
  layer: Layer<'core'>;
9
9
  correlation: string;
10
- retryable: false;
10
+ retryable: boolean;
11
11
  detail: CoreDetail | null;
12
12
  message: string | null;
13
13
  source: CustodyError | null;
@@ -62,6 +62,8 @@ export type RequestSealError = CoreError | {
62
62
  }[SimpleKind] | CustodyError | DiscoveryError | JoseError | ReplayError | AdapterError;
63
63
  /** Check whether an error belongs to the private error brand. */
64
64
  export declare function isRequestSealError(value: unknown): value is RequestSealError;
65
+ /** The bounded exception class for an internal-fault detail. */
66
+ export declare function faultClass(e: unknown): CoreDetail;
65
67
  export declare function coreError(reason: Reason<'core'>, layer: Layer<'core'>, detail?: CoreDetail | null, source?: CustodyError | null): CoreError;
66
68
  export declare const messageError: (reason: Reason<'message'>) => Readonly<{
67
69
  kind: "message";
@@ -14,13 +14,25 @@ function reasonFor(kind, reason) {
14
14
  const signatureDetails = new Set(['limit', 'wrong_message_kind', 'missing_request_context', 'unavailable_origin', 'unavailable_trailers', 'missing_field', 'non_ascii', 'unknown_field_schema', 'invalid_field_schema', 'invalid_structured_field', 'missing_dictionary_key', 'noncanonical_query_name', 'invalid_query_encoding', 'missing_query_parameter', 'ambiguous_query_parameter']);
15
15
  const agentDetails = new Set(['missing_member', 'unsupported_type', 'not_an_origin', 'untrusted_agent', 'source_mismatch', 'unknown_key', 'revoked_key', 'source_unavailable', 'timeout']);
16
16
  const fieldDetails = new Set(['legacy_string', 'invalid_dictionary', 'duplicate_member', 'invalid_member']);
17
+ // The bounded exception-class vocabulary an internal fault can name: the
18
+ // global error classes, a closed "Error" for any other Error subclass, and
19
+ // "throw" for non-error throws. No message or stack text ever enters a detail.
20
+ const internalDetails = new Set(['TypeError', 'RangeError', 'URIError', 'EvalError', 'ReferenceError', 'SyntaxError', 'Error', 'throw']);
21
+ /** The bounded exception class for an internal-fault detail. */
22
+ export function faultClass(e) {
23
+ if (e instanceof Error) {
24
+ const name = e.constructor?.name;
25
+ return typeof name === "string" && internalDetails.has(name) ? name : "Error";
26
+ }
27
+ return "throw";
28
+ }
17
29
  export function coreError(reason, layer, detail = null, source = null) {
18
30
  if (!ERROR_PARITY.some(e => e.kind === 'core' && e.layer === layer && e.reason === reason))
19
31
  throw new TypeError('Unknown core error pair');
20
32
  if (source !== null && (reason !== 'signing_failed' || !isRequestSealError(source) || source.kind !== 'custody'))
21
33
  throw new TypeError('Invalid core source');
22
- const allowed = reason === 'signature_base_failed' ? signatureDetails : reason === 'agent_unresolved' ? agentDetails : reason === 'invalid_signature_agent' ? fieldDetails : reason === 'nonqualifying_signature' ? new Set(ERROR_PARITY.filter(e => e.kind === 'core').map(e => e.reason)) : new Set();
23
- return freeze({ kind: 'core', reason, layer, correlation: correlation(), retryable: false, source, message: reason === 'signer_algorithm_mismatch' ? 'signer algorithm does not match signature specification' : null, detail: typeof detail === 'string' && allowed.has(detail) ? detail : null });
34
+ const allowed = reason === 'signature_base_failed' ? signatureDetails : reason === 'agent_unresolved' ? agentDetails : reason === 'invalid_signature_agent' ? fieldDetails : reason === 'internal' ? internalDetails : reason === 'nonqualifying_signature' ? new Set(ERROR_PARITY.filter(e => e.kind === 'core').map(e => e.reason)) : new Set();
35
+ return freeze({ kind: 'core', reason, layer, correlation: correlation(), retryable: reason === 'deadline', source, message: reason === 'signer_algorithm_mismatch' ? 'signer algorithm does not match signature specification' : null, detail: typeof detail === 'string' && allowed.has(detail) ? detail : null });
24
36
  }
25
37
  function simple(kind, reason) {
26
38
  return freeze({ kind, reason: reasonFor(kind, reason) });
@@ -15,6 +15,28 @@ export function innerCompact(input) {
15
15
  const framed = compact(input, 3, false);
16
16
  return !framed.ok && framed.error.reason === 'invalid_serialization' ? err(joseError('nesting_depth', 'nesting')) : framed;
17
17
  }
18
+ // RFC 7515 Section 4.1.10: a cty value with no "/" is read with "application/"
19
+ // prepended. The recursion-implying set is the registered JOSE media types
20
+ // (Section 9.2.1: application/jose and application/jose+json), their
21
+ // per-structure spellings application/jws and application/jwe, and RFC 7519's
22
+ // application/jwt (abbreviated "JWT"): each declares the payload is another
23
+ // JOSE envelope, which this one-level module refuses. Any other cty names
24
+ // ordinary typed payload content and is the application's business.
25
+ function nestingCty(cty) {
26
+ if (typeof cty !== "string")
27
+ return false;
28
+ // Media-type parameters do not change the type/subtype classification, so
29
+ // strip them before the abbreviation rule.
30
+ const base = cty.split(";", 1)[0].trim();
31
+ const expanded = base.includes("/") ? base : `application/${base}`;
32
+ return [
33
+ "application/jose",
34
+ "application/jose+json",
35
+ "application/jws",
36
+ "application/jwe",
37
+ "application/jwt",
38
+ ].includes(expanded.toLowerCase());
39
+ }
18
40
  /** Authenticate an outer JWE and verify its inner compact JWS. */
19
41
  export async function decrypt(input, policy, options) {
20
42
  return publicSafe(async () => {
@@ -44,7 +66,7 @@ export async function decrypt(input, policy, options) {
44
66
  const inner = await JWS.verify(plaintext, p.jws, stageOptions);
45
67
  if (!inner.ok)
46
68
  return inner;
47
- need(!inner.value.header.some((pair) => pair[0] === "cty"), "nesting_depth", "nesting");
69
+ need(!nestingCty(headerValue(inner.value.header, "cty")), "nesting_depth", "nesting");
48
70
  const result = Object.freeze({ jwe: outer.value, jws: inner.value });
49
71
  branded.add(result);
50
72
  return ok(result);
@@ -1,4 +1,5 @@
1
1
  import { byteString, encodeBase64 } from '../bytes.js';
2
+ import { digest as capturedDigest } from '../crypto/subtle.js';
2
3
  import { snapshotOptions } from '../brand.js';
3
4
  import { BODY_MAX_BYTES, MESSAGE_MAX_TARGET_BYTES, MESSAGE_MAX_FIELD_BYTES, MESSAGE_MAX_FIELDS_BYTES, MESSAGE_MAX_OCCURRENCES, MESSAGE_MAX_TOKEN_BYTES, MESSAGE_MAX_SCHEME_BYTES, MESSAGE_MAX_AUTHORITY_BYTES, MESSAGE_MAX_URL_BYTES, } from '../constants.js';
4
5
  import { messageError, isRequestSealError } from '../errors/types.js';
@@ -329,7 +330,7 @@ async function builder(kind, first, target, headers, bytes, options) {
329
330
  const digests = [];
330
331
  for (const alg of algorithms) {
331
332
  try {
332
- const hash = await crypto.subtle.digest(alg === 'sha-256' ? 'SHA-256' : 'SHA-512', new Uint8Array(content.bytes));
333
+ const hash = await capturedDigest(alg === 'sha-256' ? 'SHA-256' : 'SHA-512', new Uint8Array(content.bytes));
333
334
  digests.push(alg + '=:' + encodeBase64(new Uint8Array(hash)) + ':');
334
335
  }
335
336
  catch {
package/dist/policy.d.ts CHANGED
@@ -65,6 +65,18 @@ export declare function normalizeFreshness(input: unknown): Freshness | "not_eva
65
65
  export declare function createPolicy(input: unknown): Result<Policy>;
66
66
  /** Verification policy construction. */
67
67
  export declare const Policy: Readonly<{
68
- /** Snapshot, validate, and brand an explicit verification policy. */
68
+ /**
69
+ * Snapshot, validate, and brand an explicit verification policy.
70
+ *
71
+ * Six choices are required and none is defaulted: `algorithms`, `components`,
72
+ * `keyResolver`, `freshness`, `content`, and `replay`. Optional members are
73
+ * `extraComponents` (`"allow"` or `"reject"`), `maxSignatures` (1 through 64, default 16),
74
+ * and `fieldSchemas`.
75
+ *
76
+ * @returns `ok(policy)`, or `core` `input.invalid_policy` when any choice is missing,
77
+ * unknown, or malformed.
78
+ * @see [Signing and verifying guide](https://github.com/baselabs/request-seal-typescript/blob/main/docs/guides/signing-and-verifying.md#verify) and
79
+ * [reference](https://github.com/baselabs/request-seal-typescript/blob/main/docs/reference/sign-and-verify.md).
80
+ */
69
81
  create: typeof createPolicy;
70
82
  }>;
package/dist/policy.js CHANGED
@@ -178,6 +178,18 @@ export function createPolicy(input) {
178
178
  }
179
179
  /** Verification policy construction. */
180
180
  export const Policy = Object.freeze({
181
- /** Snapshot, validate, and brand an explicit verification policy. */
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
+ */
182
194
  create: createPolicy
183
195
  });
@@ -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),
@@ -7,5 +7,7 @@ export type Query = (sql: string, params: readonly string[], context: Readonly<{
7
7
  }>) => Promise<number>;
8
8
  /** Validate a table name and return replay-store creation SQL. */
9
9
  export declare function ddl(table: unknown): Result<string>;
10
+ /** Validate a table name and return the retention-sweep index SQL: without it the sweep's DELETE is a growing full-table scan. The name is bounded to 46 bytes so the derived index identifier fits PostgreSQL's 63-byte limit. */
11
+ export declare function ddlIndex(table: unknown): Result<string>;
10
12
  /** No database, pool, transaction, or timer starts here. Late commits remain indeterminate. */
11
13
  export declare function createStore(options: unknown): Result<ReplayStore>;
@@ -9,6 +9,12 @@ const tableName = (value) => typeof value === 'string' && value.length >= 1 && v
9
9
  export function ddl(table) {
10
10
  return tableName(table) ? ok(`CREATE TABLE "${table}" (namespace bytea NOT NULL, key bytea NOT NULL, retain_until bigint NOT NULL, PRIMARY KEY (namespace, key))`) : err(replayError('invalid_store'));
11
11
  }
12
+ /** Validate a table name and return the retention-sweep index SQL: without it the sweep's DELETE is a growing full-table scan. The name is bounded to 46 bytes so the derived index identifier fits PostgreSQL's 63-byte limit. */
13
+ export function ddlIndex(table) {
14
+ return tableName(table) && table.length <= 46
15
+ ? ok(`CREATE INDEX "${table}_retain_until_idx" ON "${table}" (retain_until)`)
16
+ : err(replayError('invalid_store'));
17
+ }
12
18
  function failure(error) {
13
19
  try {
14
20
  if (error !== null && typeof error === 'object') {
package/dist/sign.d.ts CHANGED
@@ -35,5 +35,26 @@ export type SigningSpec = Readonly<{
35
35
  digest?: readonly string[] | null;
36
36
  fieldSchemas?: unknown;
37
37
  }>;
38
- /** Sign a lossless HTTP message with explicit components and caller-owned custody. */
38
+ /**
39
+ * Sign a lossless HTTP message with explicit components and caller-owned custody.
40
+ *
41
+ * Appends one `Signature-Input` and one `Signature` field occurrence and returns a new
42
+ * branded Message. The input message is never modified, and existing signatures stay.
43
+ *
44
+ * @param message - A branded Message from `Message.request`, `Message.response`, or `Message.create`.
45
+ * @param spec - A generated spec with `label`, `components`, `algorithm`, and `expiresIn`, plus
46
+ * optional `created`, `nonce`, `alg`, `keyid`, `tag` (or the same choices nested under
47
+ * `parameters`), `digest`, and `fieldSchemas`; or an exact-input spec with only `label`,
48
+ * `signatureInput`, and `algorithm`.
49
+ * @param signer - A KeyHandle for the same algorithm, or a {@link Signer} callback.
50
+ * @param options - Generated specs accept `clock`, `nonce`, `signingTimeout`, and `context`;
51
+ * exact-input specs accept `fieldSchemas`, `signingTimeout`, and `context`. `signingTimeout`
52
+ * is 1 through 300,000 milliseconds (default 5,000).
53
+ * @returns `ok(message)`, or a `core` error such as `input.invalid_options`,
54
+ * `fields.label_in_use`, `input.signer_algorithm_mismatch`, `crypto.signing_failed`, or
55
+ * `crypto.signer_failed`.
56
+ * @see [Signing and verifying guide](https://github.com/baselabs/request-seal-typescript/blob/main/docs/guides/signing-and-verifying.md),
57
+ * [reference](https://github.com/baselabs/request-seal-typescript/blob/main/docs/reference/sign-and-verify.md), and the
58
+ * [error reasons](https://github.com/baselabs/request-seal-typescript/blob/main/docs/reference/errors.md).
59
+ */
39
60
  export declare function sign(message: unknown, spec: unknown, signer: unknown, options?: unknown): Promise<Result<HttpMessage>>;
package/dist/sign.js CHANGED
@@ -11,7 +11,7 @@ import { build } from "./signature-base/index.js";
11
11
  import { serialize, } from "./structured-fields/index.js";
12
12
  import { checkDigest, digest } from "./digest.js";
13
13
  import { fieldSchemas } from "./policy.js";
14
- import { inner, label, dictionaries, signatureSchema, parameters, fail, ensure, bounded, } from "./signature-fields.js";
14
+ import { inner, label, dictionaries, signatureSchema, parameters, fail, ensure, bounded, envelope, } from "./signature-fields.js";
15
15
  import { coreError, isRequestSealError } from "./errors/types.js";
16
16
  import { timeoutMs } from "./numbers.js";
17
17
  import { ok, err } from "./result.js";
@@ -36,7 +36,28 @@ function append(m, name, bytes) {
36
36
  result(FieldOccurrence.create({ name, value: bytes, section: "headers" })),
37
37
  ]);
38
38
  }
39
- /** Sign a lossless HTTP message with explicit components and caller-owned custody. */
39
+ /**
40
+ * Sign a lossless HTTP message with explicit components and caller-owned custody.
41
+ *
42
+ * Appends one `Signature-Input` and one `Signature` field occurrence and returns a new
43
+ * branded Message. The input message is never modified, and existing signatures stay.
44
+ *
45
+ * @param message - A branded Message from `Message.request`, `Message.response`, or `Message.create`.
46
+ * @param spec - A generated spec with `label`, `components`, `algorithm`, and `expiresIn`, plus
47
+ * optional `created`, `nonce`, `alg`, `keyid`, `tag` (or the same choices nested under
48
+ * `parameters`), `digest`, and `fieldSchemas`; or an exact-input spec with only `label`,
49
+ * `signatureInput`, and `algorithm`.
50
+ * @param signer - A KeyHandle for the same algorithm, or a {@link Signer} callback.
51
+ * @param options - Generated specs accept `clock`, `nonce`, `signingTimeout`, and `context`;
52
+ * exact-input specs accept `fieldSchemas`, `signingTimeout`, and `context`. `signingTimeout`
53
+ * is 1 through 300,000 milliseconds (default 5,000).
54
+ * @returns `ok(message)`, or a `core` error such as `input.invalid_options`,
55
+ * `fields.label_in_use`, `input.signer_algorithm_mismatch`, `crypto.signing_failed`, or
56
+ * `crypto.signer_failed`.
57
+ * @see [Signing and verifying guide](https://github.com/baselabs/request-seal-typescript/blob/main/docs/guides/signing-and-verifying.md),
58
+ * [reference](https://github.com/baselabs/request-seal-typescript/blob/main/docs/reference/sign-and-verify.md), and the
59
+ * [error reasons](https://github.com/baselabs/request-seal-typescript/blob/main/docs/reference/errors.md).
60
+ */
40
61
  export async function sign(message, spec, signer, options = {}) {
41
62
  try {
42
63
  const s = take(spec, [
@@ -117,7 +138,7 @@ export async function sign(message, spec, signer, options = {}) {
117
138
  }
118
139
  input = r.value;
119
140
  if (Object.hasOwn(o, "context") || !isKeyHandle(signer)) {
120
- c = Object.hasOwn(o, "context") ? (isContext(o.context) ? ok(o.context) : err(bounded(null, "invalid_options", "input"))) : withDeadline({ timeoutMs: 5000 });
141
+ c = Object.hasOwn(o, "context") ? (isContext(o.context) ? ok(o.context) : err(bounded(null, "invalid_options", "input"))) : withDeadline({ timeoutMs: signingTimeout });
121
142
  ensure(c.ok, "invalid_options", "input");
122
143
  }
123
144
  }
@@ -309,6 +330,6 @@ export async function sign(message, spec, signer, options = {}) {
309
330
  catch (e) {
310
331
  if (isRequestSealError(e) && e.kind === "custody" && e.reason === "deadline_exceeded")
311
332
  return err(e);
312
- return err(bounded(e, "invalid_options", "input"));
333
+ return err(envelope(e));
313
334
  }
314
335
  }
@@ -10,11 +10,15 @@ export declare function bounded(e: unknown, reason: Reason<"core">, layer: Layer
10
10
  reason: Reason<'core'>;
11
11
  layer: Layer<'core'>;
12
12
  correlation: string;
13
- retryable: false;
13
+ retryable: boolean;
14
14
  detail: CoreDetail | null;
15
15
  message: string | null;
16
16
  source: import("./errors/types.ts").CustodyError | null;
17
17
  }>;
18
+ /** The entry-point fault envelope: verify and sign are total, and an
19
+ * unexpected fault is a bounded internal error — never an escaping throw —
20
+ * with the exception class observable in detail. */
21
+ export declare function envelope(e: unknown): import("./errors/types.ts").RequestSealError;
18
22
  export declare function label(value: unknown): value is string;
19
23
  export declare const signatureSchema: Schema;
20
24
  export declare function identities(value: InnerList): string[];
@@ -1,7 +1,7 @@
1
1
  import { parse, parseField, serialize, } from "./structured-fields/index.js";
2
2
  import { byteString } from "./bytes.js";
3
3
  import { token } from "./message/index.js";
4
- import { coreError, isRequestSealError, } from "./errors/types.js";
4
+ import { coreError, isRequestSealError, faultClass, } from "./errors/types.js";
5
5
  import { ok, err } from "./result.js";
6
6
  import { SIGNATURE_MAX_BYTES, SIGNATURE_BASE_MAX_COMPONENTS, MESSAGE_MAX_TOKEN_BYTES, STRUCTURED_FIELDS_LIMITS, } from "./constants.js";
7
7
  export function fail(reason, layer, detail = null) {
@@ -16,6 +16,14 @@ export function bounded(e, reason, layer) {
16
16
  ? e
17
17
  : coreError(reason, layer);
18
18
  }
19
+ /** The entry-point fault envelope: verify and sign are total, and an
20
+ * unexpected fault is a bounded internal error — never an escaping throw —
21
+ * with the exception class observable in detail. */
22
+ export function envelope(e) {
23
+ return isRequestSealError(e)
24
+ ? e
25
+ : coreError("internal", "internal", faultClass(e));
26
+ }
19
27
  export function label(value) {
20
28
  return (typeof value === "string" &&
21
29
  value.length > 0 &&
@@ -3,7 +3,7 @@ import { isMessage, Message } from "./message/index.js";
3
3
  import { isQuorum } from "./quorum.js";
4
4
  import { isContext, withDeadline, remainingMs } from "./context.js";
5
5
  import { isAcceptSignature, applicable, } from "./accept-signature.js";
6
- import { dictionaries, parameters, identities, ensure, fail, bounded, } from "./signature-fields.js";
6
+ import { dictionaries, parameters, identities, ensure, fail, envelope, } from "./signature-fields.js";
7
7
  import { verifyCandidate } from "./verify.js";
8
8
  import { evaluateQuorum } from "./quorum/evaluation.js";
9
9
  import { completeQuorum, } from "./quorum/verification.js";
@@ -56,10 +56,30 @@ export async function verifyQuorum(message, quorum, options) {
56
56
  let qualified = false, last;
57
57
  const baseCache = new Map();
58
58
  for (const slot of slots) {
59
- const r = await verifyCandidate(m, slot.policy, {
60
- label: l,
61
- context: c.value,
62
- }, { input, signature: signatures.get(l), baseCache });
59
+ let r;
60
+ try {
61
+ r = await verifyCandidate(m, slot.policy, {
62
+ label: l,
63
+ context: c.value,
64
+ }, { input, signature: signatures.get(l), baseCache });
65
+ }
66
+ catch (fault) {
67
+ // Per-label rescue isolates a defective label as its own
68
+ // nonqualifying internal fact, never a whole-call failure.
69
+ if (isRequestSealError(fault) && fault.kind === "custody")
70
+ return err(fault);
71
+ const faulted = envelope(fault);
72
+ if (faulted.kind !== "core")
73
+ throw fault;
74
+ last = {
75
+ label: l,
76
+ slot: slot.id,
77
+ reason: faulted.reason,
78
+ layer: faulted.layer,
79
+ };
80
+ facts.push(last);
81
+ continue;
82
+ }
63
83
  if (r.ok) {
64
84
  const record = Object.freeze({
65
85
  slot,
@@ -108,8 +128,9 @@ export async function verifyQuorum(message, quorum, options) {
108
128
  const original = candidates.find((c) => c.slot === r.slot && c.label === r.label);
109
129
  results[r.label] = verified.get(original);
110
130
  }
111
- if (remainingMs(c.value) <= 0)
112
- throw custodyError("deadline_exceeded");
131
+ // The wall-clock bound lives in the assignment search: an accepted
132
+ // assignment, including one accepted at deadline exhaustion, completes.
133
+ // The remaining work here is synchronous and bounded.
113
134
  return completeQuorum({
114
135
  profile: { name: "rfc9421" },
115
136
  mode: quorum.mode,
@@ -140,6 +161,6 @@ export async function verifyQuorum(message, quorum, options) {
140
161
  catch (e) {
141
162
  if (isRequestSealError(e) && e.kind === "custody")
142
163
  return err(e);
143
- return err(bounded(e, "invalid_options", "input"));
164
+ return err(envelope(e));
144
165
  }
145
166
  }
package/dist/verify.d.ts CHANGED
@@ -20,7 +20,25 @@ type ParsedCandidate = Readonly<{
20
20
  baseCache: BaseCache;
21
21
  }>;
22
22
  export declare function verifyLabel(message: unknown, policy: unknown, options: unknown, observeIdentity?: (identity: KeyIdentity) => void, parsed?: ParsedCandidate, profile?: ReplayFacts["profile"]): Promise<Result<Verification>>;
23
- /** Verify one HTTP signature under an explicit branded policy. */
23
+ /**
24
+ * Verify one HTTP signature under an explicit branded policy.
25
+ *
26
+ * Checks exactly the named label; it never tries other labels. Success means the
27
+ * cryptography, coverage, freshness, content, and replay layers all passed. It does not
28
+ * authorize: the Verification records `authorization: "not_evaluated"`.
29
+ *
30
+ * @param message - A branded Message carrying the signature.
31
+ * @param policy - A branded Policy from `Policy.create`.
32
+ * @param options - `{label, context?, representation?, digestState?}`. `representation` is a
33
+ * complete retained Body and needs a `"representation"` content policy. `digestState` is a
34
+ * DigestState of the policy's content kind that you fed with a streamed body; verify finishes
35
+ * it. The two are mutually exclusive.
36
+ * @returns `ok(verification)`, or a bounded error such as `core` `crypto.invalid_signature`,
37
+ * `freshness.expired`, `content.digest_mismatch`, `key.unknown_key`, or `replay.replayed`.
38
+ * @see [Signing and verifying guide](https://github.com/baselabs/request-seal-typescript/blob/main/docs/guides/signing-and-verifying.md),
39
+ * [reference](https://github.com/baselabs/request-seal-typescript/blob/main/docs/reference/sign-and-verify.md), and the
40
+ * [error reasons](https://github.com/baselabs/request-seal-typescript/blob/main/docs/reference/errors.md).
41
+ */
24
42
  export declare function verify(message: unknown, policy: unknown, options: unknown): Promise<Result<Verification>>;
25
43
  /** Internal composition of the single-label pipeline; identities stay private. */
26
44
  export declare function verifyCandidate(message: unknown, policy: Policy, options: unknown, parsed?: ParsedCandidate): Promise<Result<Readonly<{