@byok-sdk/core 0.28.3 → 0.30.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/README.md +11 -0
- package/dist/index.js +18 -9
- package/dist/index.js.map +1 -1
- package/dist/sealed-payload/index.d.ts +23 -0
- package/dist/sealed-payload/index.js +1486 -0
- package/dist/sealed-payload/index.js.map +1 -0
- package/dist/sealed-payload/member-bundle.d.ts +64 -0
- package/dist/sealed-payload/member-chain-walk.d.ts +77 -0
- package/dist/sealed-payload/member-chain.d.ts +58 -0
- package/dist/sealed-payload/member-fingerprint.d.ts +14 -0
- package/dist/sealed-payload/pairing-verification.d.ts +51 -0
- package/dist/sealed-payload/primitives.d.ts +53 -0
- package/dist/sealed-payload/scope-key.d.ts +91 -0
- package/dist/sealed-payload/sealed-blob.d.ts +94 -0
- package/dist/sealed-payload/sealed-content.d.ts +186 -0
- package/dist/sealed-payload/sealed-json.d.ts +17 -0
- package/dist/total-parse.d.ts +20 -0
- package/package.json +5 -1
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sealed content v1 (#654 design §8.1, §8.2, §8.6, §9.1).
|
|
3
|
+
*
|
|
4
|
+
* A member seals the fields one category moves out of a payload. The result is
|
|
5
|
+
* a header the Host may read — version, suite, category, scope, chain head,
|
|
6
|
+
* sender, salt, nonce, padded length and content MAC — and the ciphertext
|
|
7
|
+
* bytes. Whether the ciphertext travels inline or by reference is the wire
|
|
8
|
+
* schema's rule, not this module's.
|
|
9
|
+
*
|
|
10
|
+
* The plaintext is the canonical JSON of the moved fields. It is framed before
|
|
11
|
+
* encryption: `u32be(contentLength) || content || zeros`, padded in 256-byte
|
|
12
|
+
* steps up to 16 KiB and with Padmé above. The frame is inside the AEAD, so the
|
|
13
|
+
* length and the padding are authenticated, and one content has exactly one
|
|
14
|
+
* legal padded length.
|
|
15
|
+
*
|
|
16
|
+
* The AAD binds every identity the Host could otherwise swap: tenant, scope,
|
|
17
|
+
* epoch, key id, chain head, sender, message family, direction, the write's
|
|
18
|
+
* identity, target device, Agent, the task's sealing categories, padded length
|
|
19
|
+
* and content MAC. Envelope `id`, `seq` and `ts` stay out, so an envelope that
|
|
20
|
+
* a dead-letter replay re-encodes still opens.
|
|
21
|
+
*
|
|
22
|
+
* `contentMac` is a keyed MAC over category, family, identity and plaintext. The
|
|
23
|
+
* Host compares two submissions of one identity by it, without a key.
|
|
24
|
+
*
|
|
25
|
+
* A blob field is sealed by the same frame and key rule, before its outer
|
|
26
|
+
* content. Its AAD is the outer AAD without `paddedLength` and `contentMac`,
|
|
27
|
+
* plus its own `paddedLength` and the JSON Pointer `fieldPath` to its BlobRef.
|
|
28
|
+
*/
|
|
29
|
+
import { z } from 'zod';
|
|
30
|
+
import { type JsonObject } from '../attestation';
|
|
31
|
+
import { type Bytes } from './primitives';
|
|
32
|
+
export declare const SEALED_CONTENT_VERSION = 1;
|
|
33
|
+
/** The only suite v1 admits. A new suite is a new constant and a new capability, never a parser branch. */
|
|
34
|
+
export declare const SEALED_CONTENT_SUITE = "byok-sealed-v1.hkdf-sha256.aes-256-gcm";
|
|
35
|
+
/** C1–C4, in their canonical order. A task's sealing categories are listed in this order. */
|
|
36
|
+
export declare const SEALED_CATEGORIES: readonly ['task-input', 'agent-message', 'task-output', 'memory-projection'];
|
|
37
|
+
export type SealedCategory = (typeof SEALED_CATEGORIES)[number];
|
|
38
|
+
export declare const SEALED_DIRECTIONS: readonly ['server-to-daemon', 'daemon-to-server'];
|
|
39
|
+
export type SealedDirection = (typeof SEALED_DIRECTIONS)[number];
|
|
40
|
+
export declare const SEALED_NONCE_BYTES = 12;
|
|
41
|
+
/**
|
|
42
|
+
* The one legal padded length of a frame whose unpadded length is
|
|
43
|
+
* `frameLength` (`4 + contentLength`). Up to 16 KiB: the next multiple of 256.
|
|
44
|
+
* Above: Padmé — `E = floor(log2 n)`, `S = floor(log2 E) + 1`, rounded up to a
|
|
45
|
+
* multiple of `2^(E − S)`, which adds less than 6.25%.
|
|
46
|
+
*/
|
|
47
|
+
export declare function sealedPaddedLength(frameLength: number): number;
|
|
48
|
+
/** `u32be(contentLength) || content || zeros`, as long as {@link sealedPaddedLength} says. */
|
|
49
|
+
export declare function encodeSealedFrame(content: Uint8Array): Bytes;
|
|
50
|
+
/**
|
|
51
|
+
* The content of a decrypted frame, or `undefined` when one of the five
|
|
52
|
+
* receiver checks fails: frame length equals `paddedLength`; the content fits;
|
|
53
|
+
* `paddedLength` is the padded length of exactly this content; every padding
|
|
54
|
+
* byte is zero; the content is within the message type's plaintext limit.
|
|
55
|
+
*/
|
|
56
|
+
export declare function decodeSealedFrame(frame: Uint8Array, paddedLength: number, maxContentLength: number): Uint8Array | undefined;
|
|
57
|
+
/** What the Host may read of one sealed content. The ciphertext travels next to it. */
|
|
58
|
+
export declare const SealedContentHeaderV1Schema: z.ZodObject<{
|
|
59
|
+
v: z.ZodLiteral<1>;
|
|
60
|
+
suite: z.ZodLiteral<"byok-sealed-v1.hkdf-sha256.aes-256-gcm">;
|
|
61
|
+
category: z.ZodEnum<{
|
|
62
|
+
"agent-message": "agent-message";
|
|
63
|
+
"memory-projection": "memory-projection";
|
|
64
|
+
"task-input": "task-input";
|
|
65
|
+
"task-output": "task-output";
|
|
66
|
+
}>;
|
|
67
|
+
scope: z.ZodObject<{
|
|
68
|
+
id: z.ZodString;
|
|
69
|
+
epoch: z.ZodNumber;
|
|
70
|
+
keyId: z.ZodString;
|
|
71
|
+
}, z.core.$strict>;
|
|
72
|
+
chainHead: z.ZodObject<{
|
|
73
|
+
seq: z.ZodNumber;
|
|
74
|
+
hash: z.ZodString;
|
|
75
|
+
}, z.core.$strict>;
|
|
76
|
+
sender: z.ZodString;
|
|
77
|
+
salt: z.ZodString;
|
|
78
|
+
nonce: z.ZodString;
|
|
79
|
+
paddedLength: z.ZodNumber;
|
|
80
|
+
contentMac: z.ZodString;
|
|
81
|
+
}, z.core.$strict>;
|
|
82
|
+
export type SealedContentHeaderV1 = z.infer<typeof SealedContentHeaderV1Schema>;
|
|
83
|
+
/**
|
|
84
|
+
* What the receiver knows from its own context and the payload's plaintext
|
|
85
|
+
* fields. The receiver builds it; the sealed bytes never supply it.
|
|
86
|
+
*/
|
|
87
|
+
export interface SealedBinding {
|
|
88
|
+
readonly tenantId: string;
|
|
89
|
+
/** Message family, e.g. `task.offer_for_agent`, or `routine.template`. */
|
|
90
|
+
readonly family: string;
|
|
91
|
+
readonly direction: SealedDirection;
|
|
92
|
+
/** The write's identity: `{ taskId }`, `{ messageId }`, … or `{ routineId, definitionVersion }`. */
|
|
93
|
+
readonly identity: {
|
|
94
|
+
readonly [field: string]: string | number;
|
|
95
|
+
};
|
|
96
|
+
/** Target device. `null` only for the `routine.template` family, which binds no device. */
|
|
97
|
+
readonly deviceId: string | null;
|
|
98
|
+
readonly agentRef: {
|
|
99
|
+
readonly agentId: string;
|
|
100
|
+
readonly profileRevision: string;
|
|
101
|
+
} | null;
|
|
102
|
+
/** The task's `sealing.categories`, unique and in {@link SEALED_CATEGORIES} order. */
|
|
103
|
+
readonly categories: readonly SealedCategory[];
|
|
104
|
+
/** A `routine.template`'s expiry; `null` for every other family. */
|
|
105
|
+
readonly notAfter: string | null;
|
|
106
|
+
}
|
|
107
|
+
/** The canonical AAD bytes of a header under a binding (§8.2). */
|
|
108
|
+
export declare function sealedContentAad(header: SealedContentHeaderV1, binding: SealedBinding): Uint8Array;
|
|
109
|
+
/** The content MAC of one plaintext under one scope key (§9.1). */
|
|
110
|
+
export declare function sealedContentMac(input: {
|
|
111
|
+
readonly scopeKey: Uint8Array;
|
|
112
|
+
readonly category: SealedCategory;
|
|
113
|
+
readonly binding: SealedBinding;
|
|
114
|
+
readonly plaintext: JsonObject;
|
|
115
|
+
}): Promise<string>;
|
|
116
|
+
export interface SealContentInput {
|
|
117
|
+
readonly scopeKey: Uint8Array;
|
|
118
|
+
readonly scope: {
|
|
119
|
+
readonly id: string;
|
|
120
|
+
readonly epoch: number;
|
|
121
|
+
};
|
|
122
|
+
/** The member chain head the sender verified before sealing. */
|
|
123
|
+
readonly chainHead: {
|
|
124
|
+
readonly seq: number;
|
|
125
|
+
readonly hash: string;
|
|
126
|
+
};
|
|
127
|
+
/** The sender's member id. */
|
|
128
|
+
readonly sender: string;
|
|
129
|
+
readonly category: SealedCategory;
|
|
130
|
+
readonly binding: SealedBinding;
|
|
131
|
+
/** The fields this category moves out of the payload. */
|
|
132
|
+
readonly plaintext: JsonObject;
|
|
133
|
+
/** The plaintext limit of this message type, in UTF-8 bytes of canonical JSON. */
|
|
134
|
+
readonly maxContentLength: number;
|
|
135
|
+
}
|
|
136
|
+
export interface SealedContent {
|
|
137
|
+
readonly header: SealedContentHeaderV1;
|
|
138
|
+
/** AES-256-GCM output: the padded frame followed by the 16-byte tag. */
|
|
139
|
+
readonly ciphertext: Uint8Array;
|
|
140
|
+
}
|
|
141
|
+
/** Seal one category of a payload once. A retry must resend these bytes, never seal again (§9.2). */
|
|
142
|
+
export declare function sealContent(input: SealContentInput): Promise<SealedContent>;
|
|
143
|
+
/** The receiver failure codes of design §8.3. */
|
|
144
|
+
export type SealedContentOpenFailure = 'sealed_key_unavailable' | 'sealed_open_failed' | 'sealed_content_mac_mismatch' | 'sealing_policy_mismatch';
|
|
145
|
+
/**
|
|
146
|
+
* An opened content, with the header that the AEAD authenticated. The caller
|
|
147
|
+
* reads `chainHead` and `sender` from it for the sender-view check (§7.3).
|
|
148
|
+
*/
|
|
149
|
+
export type SealedContentOpenResult = {
|
|
150
|
+
readonly ok: true;
|
|
151
|
+
readonly plaintext: JsonObject;
|
|
152
|
+
readonly header: SealedContentHeaderV1;
|
|
153
|
+
} | {
|
|
154
|
+
readonly ok: false;
|
|
155
|
+
readonly reason: SealedContentOpenFailure;
|
|
156
|
+
};
|
|
157
|
+
/** The task's frozen `sealing` scope (§6.2). Its `categories` are {@link SealedBinding.categories}. */
|
|
158
|
+
export interface SealedScopePolicy {
|
|
159
|
+
readonly scopeId: string;
|
|
160
|
+
/** The lowest epoch the task accepts: the epoch of the offer's C1 content. */
|
|
161
|
+
readonly epoch: number;
|
|
162
|
+
}
|
|
163
|
+
export interface OpenSealedContentInput {
|
|
164
|
+
/** The header as received. It is untrusted. */
|
|
165
|
+
readonly header: unknown;
|
|
166
|
+
readonly ciphertext: Uint8Array;
|
|
167
|
+
/** The scope key the receiver holds for `header.scope`. */
|
|
168
|
+
readonly scopeKey: Uint8Array;
|
|
169
|
+
/** Partly written by the Host (`sealing.categories`, `notAfter`), so it is untrusted too. */
|
|
170
|
+
readonly binding: SealedBinding;
|
|
171
|
+
/** From the task's `sealing`, which the Host writes. Untrusted. */
|
|
172
|
+
readonly sealing: SealedScopePolicy;
|
|
173
|
+
readonly maxContentLength: number;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Open one sealed content, fail closed. In order: a binding and a task scope
|
|
177
|
+
* that parse (`sealing_policy_mismatch`); header shape (`sealed_open_failed`);
|
|
178
|
+
* the category within the task's categories, and the header scope equal to
|
|
179
|
+
* the task's scope at its epoch or later (both `sealing_policy_mismatch`); key
|
|
180
|
+
* id of the held scope key (`sealed_key_unavailable`); ciphertext length; AEAD
|
|
181
|
+
* under the rebuilt AAD; the five frame checks; canonical JSON of a plain
|
|
182
|
+
* object (all `sealed_open_failed`); and the content MAC
|
|
183
|
+
* (`sealed_content_mac_mismatch`).
|
|
184
|
+
* There is no plaintext fallback, and nothing here throws for hostile input.
|
|
185
|
+
*/
|
|
186
|
+
export declare function openSealedContent(input: OpenSealedContentInput): Promise<SealedContentOpenResult>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical JSON of a sealed plaintext (#786): RFC 8785 with its full number
|
|
3
|
+
* rule.
|
|
4
|
+
*
|
|
5
|
+
* Keys sort by UTF-16 code unit and strings use the ECMAScript serialization,
|
|
6
|
+
* the same as `canonicalizeJson`. Numbers differ. A finite number serializes
|
|
7
|
+
* as `Number.prototype.toString` (RFC 8785 §3.2.2.3), so a decimal, an
|
|
8
|
+
* exponent and an integer above 2^53 are sealable. `-0` serializes as `0`.
|
|
9
|
+
* NaN and the infinities have no JSON form and throw.
|
|
10
|
+
*
|
|
11
|
+
* Only the sealed plaintext uses this rule. The AAD, the MAC identity, member
|
|
12
|
+
* chain entries and key packages keep `canonicalizeJson`, whose safe-integer
|
|
13
|
+
* rule belongs to the attestation contract.
|
|
14
|
+
*/
|
|
15
|
+
import type { JsonValue } from '../attestation';
|
|
16
|
+
/** The canonical JSON text of a sealed plaintext value. Throws `sealed_secret_invalid` outside JSON. */
|
|
17
|
+
export declare function canonicalizeSealedJson(value: JsonValue): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A zod parse that never throws.
|
|
3
|
+
*
|
|
4
|
+
* `safeParse` returns a failure for invalid data, but it does not catch an
|
|
5
|
+
* error that a getter or a proxy trap throws while zod reads the value, and a
|
|
6
|
+
* revoked proxy throws on any read. A receiver that promises a typed result
|
|
7
|
+
* for hostile input, or a typed error for bad caller input, parses through
|
|
8
|
+
* this. Wire input from `JSON.parse` cannot hold getters or proxies; this
|
|
9
|
+
* closes the same gap for in-process callers.
|
|
10
|
+
*/
|
|
11
|
+
import type { z } from 'zod';
|
|
12
|
+
export type TotalParseResult<T> = {
|
|
13
|
+
readonly success: true;
|
|
14
|
+
readonly data: T;
|
|
15
|
+
} | {
|
|
16
|
+
readonly success: false;
|
|
17
|
+
readonly error?: z.ZodError;
|
|
18
|
+
};
|
|
19
|
+
/** `schema.safeParse(value)`, with a throw while reading `value` as a failure without issues. */
|
|
20
|
+
export declare function safeParseTotal<S extends z.ZodType>(schema: S, value: unknown): TotalParseResult<z.output<S>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byok-sdk/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "BYOK SDK platform contracts: branded tenant identity, tenant-first store ports, device proof canonicalization, and the composition conformance reference",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
"types": "./dist/index.d.ts",
|
|
26
26
|
"import": "./dist/index.js"
|
|
27
27
|
},
|
|
28
|
+
"./sealed-payload": {
|
|
29
|
+
"types": "./dist/sealed-payload/index.d.ts",
|
|
30
|
+
"import": "./dist/sealed-payload/index.js"
|
|
31
|
+
},
|
|
28
32
|
"./package.json": "./package.json"
|
|
29
33
|
},
|
|
30
34
|
"files": [
|