@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.
- package/CHANGELOG.md +34 -1
- package/README.md +10 -9
- package/dist/adapters/node-http/index.d.ts +8 -1
- package/dist/adapters/node-http/index.js +21 -5
- package/dist/constants.d.ts +4 -4
- package/dist/constants.js +2 -2
- package/dist/crypto/algorithm.d.ts +4 -0
- package/dist/crypto/algorithm.js +12 -0
- package/dist/discovery/body.d.ts +12 -2
- package/dist/discovery/body.js +33 -11
- package/dist/discovery/cache.d.ts +13 -3
- package/dist/discovery/cache.js +103 -39
- package/dist/discovery/headers.d.ts +2 -0
- package/dist/discovery/headers.js +27 -1
- package/dist/discovery/index.d.ts +9 -2
- package/dist/discovery/index.js +22 -6
- package/dist/discovery/node.d.ts +6 -2
- package/dist/discovery/node.js +124 -18
- package/dist/discovery/proof.js +2 -2
- package/dist/discovery/snapshot.js +3 -0
- package/dist/errors/parity.d.ts +29 -3
- package/dist/errors/parity.js +32 -3
- package/dist/errors/types.d.ts +4 -2
- package/dist/errors/types.js +14 -2
- package/dist/jose/nested.js +23 -1
- package/dist/message/index.js +2 -1
- package/dist/policy.d.ts +13 -1
- package/dist/policy.js +13 -1
- package/dist/quorum/evaluation.d.ts +1 -1
- package/dist/quorum/evaluation.js +18 -4
- package/dist/replay/postgres.d.ts +2 -0
- package/dist/replay/postgres.js +6 -0
- package/dist/sign.d.ts +22 -1
- package/dist/sign.js +25 -4
- package/dist/signature-fields.d.ts +5 -1
- package/dist/signature-fields.js +9 -1
- package/dist/verify-quorum.js +29 -8
- package/dist/verify.d.ts +19 -1
- package/dist/verify.js +32 -5
- package/dist/web-bot-auth/verify.js +2 -2
- package/package.json +13 -1
package/dist/errors/parity.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The immutable reference release used for the error vocabulary. */
|
|
2
|
-
export declare const ELIXIR_ERROR_PIN = "
|
|
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:
|
|
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:
|
|
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";
|
package/dist/errors/parity.js
CHANGED
|
@@ -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 = '
|
|
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":
|
|
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":
|
|
1940
|
+
"line": 517
|
|
1912
1941
|
},
|
|
1913
1942
|
"unknown_key": {
|
|
1914
1943
|
"kind": "constant",
|
package/dist/errors/types.d.ts
CHANGED
|
@@ -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:
|
|
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";
|
package/dist/errors/types.js
CHANGED
|
@@ -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:
|
|
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) });
|
package/dist/jose/nested.js
CHANGED
|
@@ -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
|
|
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);
|
package/dist/message/index.js
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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:
|
|
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 =
|
|
252
|
+
exhausted = "nodes";
|
|
253
253
|
return;
|
|
254
254
|
}
|
|
255
255
|
nodes++;
|
|
256
256
|
if (now() - sliceStart >= 10) {
|
|
257
257
|
yield;
|
|
258
|
-
|
|
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
|
-
|
|
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>;
|
package/dist/replay/postgres.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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:
|
|
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(
|
|
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:
|
|
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[];
|
package/dist/signature-fields.js
CHANGED
|
@@ -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 &&
|
package/dist/verify-quorum.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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(
|
|
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
|
-
/**
|
|
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<{
|