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