@byok-sdk/core 0.28.2 → 0.29.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/sealed-payload/index.d.ts +15 -0
- package/dist/sealed-payload/index.js +1048 -0
- package/dist/sealed-payload/index.js.map +1 -0
- package/dist/sealed-payload/member-chain.d.ts +58 -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-content.d.ts +185 -0
- package/package.json +5 -1
|
@@ -0,0 +1,185 @@
|
|
|
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
|
+
* Blob fields are not sealed here yet; their AAD rule comes with the blob wire
|
|
26
|
+
* rules.
|
|
27
|
+
*/
|
|
28
|
+
import { z } from 'zod';
|
|
29
|
+
import { type JsonObject } from '../attestation';
|
|
30
|
+
import { type Bytes } from './primitives';
|
|
31
|
+
export declare const SEALED_CONTENT_VERSION = 1;
|
|
32
|
+
/** The only suite v1 admits. A new suite is a new constant and a new capability, never a parser branch. */
|
|
33
|
+
export declare const SEALED_CONTENT_SUITE = "byok-sealed-v1.hkdf-sha256.aes-256-gcm";
|
|
34
|
+
/** C1–C4, in their canonical order. A task's sealing categories are listed in this order. */
|
|
35
|
+
export declare const SEALED_CATEGORIES: readonly ['task-input', 'agent-message', 'task-output', 'memory-projection'];
|
|
36
|
+
export type SealedCategory = (typeof SEALED_CATEGORIES)[number];
|
|
37
|
+
export declare const SEALED_DIRECTIONS: readonly ['server-to-daemon', 'daemon-to-server'];
|
|
38
|
+
export type SealedDirection = (typeof SEALED_DIRECTIONS)[number];
|
|
39
|
+
export declare const SEALED_NONCE_BYTES = 12;
|
|
40
|
+
/**
|
|
41
|
+
* The one legal padded length of a frame whose unpadded length is
|
|
42
|
+
* `frameLength` (`4 + contentLength`). Up to 16 KiB: the next multiple of 256.
|
|
43
|
+
* Above: Padmé — `E = floor(log2 n)`, `S = floor(log2 E) + 1`, rounded up to a
|
|
44
|
+
* multiple of `2^(E − S)`, which adds less than 6.25%.
|
|
45
|
+
*/
|
|
46
|
+
export declare function sealedPaddedLength(frameLength: number): number;
|
|
47
|
+
/** `u32be(contentLength) || content || zeros`, as long as {@link sealedPaddedLength} says. */
|
|
48
|
+
export declare function encodeSealedFrame(content: Uint8Array): Bytes;
|
|
49
|
+
/**
|
|
50
|
+
* The content of a decrypted frame, or `undefined` when one of the five
|
|
51
|
+
* receiver checks fails: frame length equals `paddedLength`; the content fits;
|
|
52
|
+
* `paddedLength` is the padded length of exactly this content; every padding
|
|
53
|
+
* byte is zero; the content is within the message type's plaintext limit.
|
|
54
|
+
*/
|
|
55
|
+
export declare function decodeSealedFrame(frame: Uint8Array, paddedLength: number, maxContentLength: number): Uint8Array | undefined;
|
|
56
|
+
/** What the Host may read of one sealed content. The ciphertext travels next to it. */
|
|
57
|
+
export declare const SealedContentHeaderV1Schema: z.ZodObject<{
|
|
58
|
+
v: z.ZodLiteral<1>;
|
|
59
|
+
suite: z.ZodLiteral<"byok-sealed-v1.hkdf-sha256.aes-256-gcm">;
|
|
60
|
+
category: z.ZodEnum<{
|
|
61
|
+
"agent-message": "agent-message";
|
|
62
|
+
"memory-projection": "memory-projection";
|
|
63
|
+
"task-input": "task-input";
|
|
64
|
+
"task-output": "task-output";
|
|
65
|
+
}>;
|
|
66
|
+
scope: z.ZodObject<{
|
|
67
|
+
id: z.ZodString;
|
|
68
|
+
epoch: z.ZodNumber;
|
|
69
|
+
keyId: z.ZodString;
|
|
70
|
+
}, z.core.$strict>;
|
|
71
|
+
chainHead: z.ZodObject<{
|
|
72
|
+
seq: z.ZodNumber;
|
|
73
|
+
hash: z.ZodString;
|
|
74
|
+
}, z.core.$strict>;
|
|
75
|
+
sender: z.ZodString;
|
|
76
|
+
salt: z.ZodString;
|
|
77
|
+
nonce: z.ZodString;
|
|
78
|
+
paddedLength: z.ZodNumber;
|
|
79
|
+
contentMac: z.ZodString;
|
|
80
|
+
}, z.core.$strict>;
|
|
81
|
+
export type SealedContentHeaderV1 = z.infer<typeof SealedContentHeaderV1Schema>;
|
|
82
|
+
/**
|
|
83
|
+
* What the receiver knows from its own context and the payload's plaintext
|
|
84
|
+
* fields. The receiver builds it; the sealed bytes never supply it.
|
|
85
|
+
*/
|
|
86
|
+
export interface SealedBinding {
|
|
87
|
+
readonly tenantId: string;
|
|
88
|
+
/** Message family, e.g. `task.offer_for_agent`, or `routine.template`. */
|
|
89
|
+
readonly family: string;
|
|
90
|
+
readonly direction: SealedDirection;
|
|
91
|
+
/** The write's identity: `{ taskId }`, `{ messageId }`, … or `{ routineId, definitionVersion }`. */
|
|
92
|
+
readonly identity: {
|
|
93
|
+
readonly [field: string]: string | number;
|
|
94
|
+
};
|
|
95
|
+
/** Target device. `null` only for the `routine.template` family, which binds no device. */
|
|
96
|
+
readonly deviceId: string | null;
|
|
97
|
+
readonly agentRef: {
|
|
98
|
+
readonly agentId: string;
|
|
99
|
+
readonly profileRevision: string;
|
|
100
|
+
} | null;
|
|
101
|
+
/** The task's `sealing.categories`, unique and in {@link SEALED_CATEGORIES} order. */
|
|
102
|
+
readonly categories: readonly SealedCategory[];
|
|
103
|
+
/** A `routine.template`'s expiry; `null` for every other family. */
|
|
104
|
+
readonly notAfter: string | null;
|
|
105
|
+
}
|
|
106
|
+
/** The canonical AAD bytes of a header under a binding (§8.2). */
|
|
107
|
+
export declare function sealedContentAad(header: SealedContentHeaderV1, binding: SealedBinding): Uint8Array;
|
|
108
|
+
/** The content MAC of one plaintext under one scope key (§9.1). */
|
|
109
|
+
export declare function sealedContentMac(input: {
|
|
110
|
+
readonly scopeKey: Uint8Array;
|
|
111
|
+
readonly category: SealedCategory;
|
|
112
|
+
readonly binding: SealedBinding;
|
|
113
|
+
readonly plaintext: JsonObject;
|
|
114
|
+
}): Promise<string>;
|
|
115
|
+
export interface SealContentInput {
|
|
116
|
+
readonly scopeKey: Uint8Array;
|
|
117
|
+
readonly scope: {
|
|
118
|
+
readonly id: string;
|
|
119
|
+
readonly epoch: number;
|
|
120
|
+
};
|
|
121
|
+
/** The member chain head the sender verified before sealing. */
|
|
122
|
+
readonly chainHead: {
|
|
123
|
+
readonly seq: number;
|
|
124
|
+
readonly hash: string;
|
|
125
|
+
};
|
|
126
|
+
/** The sender's member id. */
|
|
127
|
+
readonly sender: string;
|
|
128
|
+
readonly category: SealedCategory;
|
|
129
|
+
readonly binding: SealedBinding;
|
|
130
|
+
/** The fields this category moves out of the payload. */
|
|
131
|
+
readonly plaintext: JsonObject;
|
|
132
|
+
/** The plaintext limit of this message type, in UTF-8 bytes of canonical JSON. */
|
|
133
|
+
readonly maxContentLength: number;
|
|
134
|
+
}
|
|
135
|
+
export interface SealedContent {
|
|
136
|
+
readonly header: SealedContentHeaderV1;
|
|
137
|
+
/** AES-256-GCM output: the padded frame followed by the 16-byte tag. */
|
|
138
|
+
readonly ciphertext: Uint8Array;
|
|
139
|
+
}
|
|
140
|
+
/** Seal one category of a payload once. A retry must resend these bytes, never seal again (§9.2). */
|
|
141
|
+
export declare function sealContent(input: SealContentInput): Promise<SealedContent>;
|
|
142
|
+
/** The receiver failure codes of design §8.3. */
|
|
143
|
+
export type SealedContentOpenFailure = 'sealed_key_unavailable' | 'sealed_open_failed' | 'sealed_content_mac_mismatch' | 'sealing_policy_mismatch';
|
|
144
|
+
/**
|
|
145
|
+
* An opened content, with the header that the AEAD authenticated. The caller
|
|
146
|
+
* reads `chainHead` and `sender` from it for the sender-view check (§7.3).
|
|
147
|
+
*/
|
|
148
|
+
export type SealedContentOpenResult = {
|
|
149
|
+
readonly ok: true;
|
|
150
|
+
readonly plaintext: JsonObject;
|
|
151
|
+
readonly header: SealedContentHeaderV1;
|
|
152
|
+
} | {
|
|
153
|
+
readonly ok: false;
|
|
154
|
+
readonly reason: SealedContentOpenFailure;
|
|
155
|
+
};
|
|
156
|
+
/** The task's frozen `sealing` scope (§6.2). Its `categories` are {@link SealedBinding.categories}. */
|
|
157
|
+
export interface SealedScopePolicy {
|
|
158
|
+
readonly scopeId: string;
|
|
159
|
+
/** The lowest epoch the task accepts: the epoch of the offer's C1 content. */
|
|
160
|
+
readonly epoch: number;
|
|
161
|
+
}
|
|
162
|
+
export interface OpenSealedContentInput {
|
|
163
|
+
/** The header as received. It is untrusted. */
|
|
164
|
+
readonly header: unknown;
|
|
165
|
+
readonly ciphertext: Uint8Array;
|
|
166
|
+
/** The scope key the receiver holds for `header.scope`. */
|
|
167
|
+
readonly scopeKey: Uint8Array;
|
|
168
|
+
/** Partly written by the Host (`sealing.categories`, `notAfter`), so it is untrusted too. */
|
|
169
|
+
readonly binding: SealedBinding;
|
|
170
|
+
/** From the task's `sealing`, which the Host writes. Untrusted. */
|
|
171
|
+
readonly sealing: SealedScopePolicy;
|
|
172
|
+
readonly maxContentLength: number;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Open one sealed content, fail closed. In order: a binding and a task scope
|
|
176
|
+
* that parse (`sealing_policy_mismatch`); header shape (`sealed_open_failed`);
|
|
177
|
+
* the category within the task's categories, and the header scope equal to
|
|
178
|
+
* the task's scope at its epoch or later (both `sealing_policy_mismatch`); key
|
|
179
|
+
* id of the held scope key (`sealed_key_unavailable`); ciphertext length; AEAD
|
|
180
|
+
* under the rebuilt AAD; the five frame checks; canonical JSON of a plain
|
|
181
|
+
* object (all `sealed_open_failed`); and the content MAC
|
|
182
|
+
* (`sealed_content_mac_mismatch`).
|
|
183
|
+
* There is no plaintext fallback, and nothing here throws for hostile input.
|
|
184
|
+
*/
|
|
185
|
+
export declare function openSealedContent(input: OpenSealedContentInput): Promise<SealedContentOpenResult>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byok-sdk/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.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": [
|