@request-seal/core 0.2.0 → 0.4.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 +37 -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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/jose/nested.js +23 -1
- package/dist/key_handle.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 +28 -6
- package/dist/signature-fields.d.ts +5 -1
- package/dist/signature-fields.js +9 -1
- package/dist/telemetry.d.ts +43 -0
- package/dist/telemetry.js +86 -0
- package/dist/verify-quorum.js +34 -9
- package/dist/verify.d.ts +19 -1
- package/dist/verify.js +48 -6
- package/dist/web-bot-auth/verify.js +7 -3
- 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/index.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ export { KeyManagement, type KeyManagementHandle, isKeyManagementHandle, type Wr
|
|
|
36
36
|
export { Nested, isNestedResult, type NestedResult } from './jose/nested.ts';
|
|
37
37
|
export { Claim, Replay, createClaim, isReplayClaim, isReplayReceipt, createReplayStore, isReplayStore } from './replay/index.ts';
|
|
38
38
|
export { createMemoryStore } from './replay/memory.ts';
|
|
39
|
+
export { Telemetry, type TelemetryEvent, type TelemetryEventName, type TelemetryHandle, type TelemetryOutcome, type TelemetrySubscriber } from './telemetry.ts';
|
|
39
40
|
export * from './discovery/index.ts';
|
|
40
41
|
export { Quorum, createQuorum, isQuorum } from './quorum.ts';
|
|
41
42
|
export type { QuorumMode, QuorumUnit, SignerSlot, QuorumBinding } from './quorum.ts';
|
package/dist/index.js
CHANGED
|
@@ -27,6 +27,7 @@ export { KeyManagement, isKeyManagementHandle } from './jose/key-management.js';
|
|
|
27
27
|
export { Nested, isNestedResult } from './jose/nested.js';
|
|
28
28
|
export { Claim, Replay, createClaim, isReplayClaim, isReplayReceipt, createReplayStore, isReplayStore } from './replay/index.js';
|
|
29
29
|
export { createMemoryStore } from './replay/memory.js';
|
|
30
|
+
export { Telemetry } from './telemetry.js';
|
|
30
31
|
export * from './discovery/index.js';
|
|
31
32
|
export { Quorum, createQuorum, isQuorum } from './quorum.js';
|
|
32
33
|
export { AcceptSignature, parseAcceptSignature, serializeAcceptSignature, fulfillAcceptSignature, isAcceptSignature } from './accept-signature.js';
|
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/key_handle.js
CHANGED
|
@@ -10,6 +10,7 @@ import { dataBytes, boundedBinary, binaryByteLength } from './crypto/primitive.j
|
|
|
10
10
|
import { isPublicKey } from './public_key.js';
|
|
11
11
|
import { isKeyIdentity } from './key_identity.js';
|
|
12
12
|
import { isUnwrappedKey } from './custody/content_key.js';
|
|
13
|
+
import { emit, monotonicMs, span } from "./telemetry.js";
|
|
13
14
|
const handles = new WeakSet();
|
|
14
15
|
const token = Object.freeze({});
|
|
15
16
|
/** Check the private brand of a custody handle. */
|
|
@@ -75,7 +76,28 @@ class ManagedHandle {
|
|
|
75
76
|
return resolved.ok && resolved.value.id === this.#spec.id;
|
|
76
77
|
}
|
|
77
78
|
async sign(algorithm, data, context, options = {}) {
|
|
78
|
-
|
|
79
|
+
// Byte options precede every handle touch, so a hostile receiver with bad
|
|
80
|
+
// options still reports invalid_options; the span covers the whole call.
|
|
81
|
+
const early = byteLimit(options);
|
|
82
|
+
const start = monotonicMs();
|
|
83
|
+
if (!early.ok) {
|
|
84
|
+
const error = custodyError('invalid_options');
|
|
85
|
+
emit("custodySign", monotonicMs() - start, "error", error);
|
|
86
|
+
return err(error);
|
|
87
|
+
}
|
|
88
|
+
return span("custodySign", () => {
|
|
89
|
+
try {
|
|
90
|
+
return this == null ? Promise.resolve(err(custodyError('invalid_data'))) : this.#signSpanned(algorithm, data, context, early);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// An unbranded receiver cannot reach the private path; the failure
|
|
94
|
+
// stays a bounded custody error, never an escaping TypeError.
|
|
95
|
+
return Promise.resolve(err(custodyError('invalid_data')));
|
|
96
|
+
}
|
|
97
|
+
}, r => r.ok ? "ok" : "error", r => r.ok ? undefined : r.error);
|
|
98
|
+
}
|
|
99
|
+
async #signSpanned(algorithm, data, context, maximum) {
|
|
100
|
+
// Options were read once and validated by the public wrapper.
|
|
79
101
|
if (!maximum.ok)
|
|
80
102
|
return err(custodyError('invalid_options'));
|
|
81
103
|
try {
|
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,10 +11,11 @@ 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";
|
|
18
|
+
import { span } from "./telemetry.js";
|
|
18
19
|
function take(input, keys) {
|
|
19
20
|
const r = snapshotOptions(input, keys);
|
|
20
21
|
ensure(r.ok, "invalid_options", "input");
|
|
@@ -36,7 +37,28 @@ function append(m, name, bytes) {
|
|
|
36
37
|
result(FieldOccurrence.create({ name, value: bytes, section: "headers" })),
|
|
37
38
|
]);
|
|
38
39
|
}
|
|
39
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Sign a lossless HTTP message with explicit components and caller-owned custody.
|
|
42
|
+
*
|
|
43
|
+
* Appends one `Signature-Input` and one `Signature` field occurrence and returns a new
|
|
44
|
+
* branded Message. The input message is never modified, and existing signatures stay.
|
|
45
|
+
*
|
|
46
|
+
* @param message - A branded Message from `Message.request`, `Message.response`, or `Message.create`.
|
|
47
|
+
* @param spec - A generated spec with `label`, `components`, `algorithm`, and `expiresIn`, plus
|
|
48
|
+
* optional `created`, `nonce`, `alg`, `keyid`, `tag` (or the same choices nested under
|
|
49
|
+
* `parameters`), `digest`, and `fieldSchemas`; or an exact-input spec with only `label`,
|
|
50
|
+
* `signatureInput`, and `algorithm`.
|
|
51
|
+
* @param signer - A KeyHandle for the same algorithm, or a {@link Signer} callback.
|
|
52
|
+
* @param options - Generated specs accept `clock`, `nonce`, `signingTimeout`, and `context`;
|
|
53
|
+
* exact-input specs accept `fieldSchemas`, `signingTimeout`, and `context`. `signingTimeout`
|
|
54
|
+
* is 1 through 300,000 milliseconds (default 5,000).
|
|
55
|
+
* @returns `ok(message)`, or a `core` error such as `input.invalid_options`,
|
|
56
|
+
* `fields.label_in_use`, `input.signer_algorithm_mismatch`, `crypto.signing_failed`, or
|
|
57
|
+
* `crypto.signer_failed`.
|
|
58
|
+
* @see [Signing and verifying guide](https://github.com/baselabs/request-seal-typescript/blob/main/docs/guides/signing-and-verifying.md),
|
|
59
|
+
* [reference](https://github.com/baselabs/request-seal-typescript/blob/main/docs/reference/sign-and-verify.md), and the
|
|
60
|
+
* [error reasons](https://github.com/baselabs/request-seal-typescript/blob/main/docs/reference/errors.md).
|
|
61
|
+
*/
|
|
40
62
|
export async function sign(message, spec, signer, options = {}) {
|
|
41
63
|
try {
|
|
42
64
|
const s = take(spec, [
|
|
@@ -117,7 +139,7 @@ export async function sign(message, spec, signer, options = {}) {
|
|
|
117
139
|
}
|
|
118
140
|
input = r.value;
|
|
119
141
|
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:
|
|
142
|
+
c = Object.hasOwn(o, "context") ? (isContext(o.context) ? ok(o.context) : err(bounded(null, "invalid_options", "input"))) : withDeadline({ timeoutMs: signingTimeout });
|
|
121
143
|
ensure(c.ok, "invalid_options", "input");
|
|
122
144
|
}
|
|
123
145
|
}
|
|
@@ -254,7 +276,7 @@ export async function sign(message, spec, signer, options = {}) {
|
|
|
254
276
|
try {
|
|
255
277
|
signed = isKeyHandle(signer)
|
|
256
278
|
? await custodySign(signer, algorithm, base.value, c.value)
|
|
257
|
-
: await run(c.value, async (context) => {
|
|
279
|
+
: await span("custodySign", () => run(c.value, async (context) => {
|
|
258
280
|
try {
|
|
259
281
|
const r = await signer(algorithm, new Uint8Array(base.value), context);
|
|
260
282
|
if (!r ||
|
|
@@ -268,7 +290,7 @@ export async function sign(message, spec, signer, options = {}) {
|
|
|
268
290
|
catch {
|
|
269
291
|
return err(bounded(null, "signer_failed", "crypto"));
|
|
270
292
|
}
|
|
271
|
-
});
|
|
293
|
+
}), r => r.ok ? "ok" : "error", r => r.ok ? undefined : r.error);
|
|
272
294
|
}
|
|
273
295
|
finally {
|
|
274
296
|
if (ownedSigningContext)
|
|
@@ -309,6 +331,6 @@ export async function sign(message, spec, signer, options = {}) {
|
|
|
309
331
|
catch (e) {
|
|
310
332
|
if (isRequestSealError(e) && e.kind === "custody" && e.reason === "deadline_exceeded")
|
|
311
333
|
return err(e);
|
|
312
|
-
return err(
|
|
334
|
+
return err(envelope(e));
|
|
313
335
|
}
|
|
314
336
|
}
|
|
@@ -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 &&
|