@world-engines/ladybug-bridge 0.1.0-alpha.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/LICENSE +46 -0
- package/README.md +3 -0
- package/dist/index.d.ts +704 -0
- package/dist/index.js +3781 -0
- package/dist/view-access-policy.d.ts +153 -0
- package/dist/view-access-policy.js +835 -0
- package/package.json +53 -0
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
export const VIEW_ACCESS_POLICY_SCHEMA_VERSION = 2;
|
|
2
|
+
export const VIEW_ACCESS_POLICY_ROOT_SUBJECT_ID = "view_policy_root_v2";
|
|
3
|
+
const MAX_IDENTIFIER_BYTES = 512;
|
|
4
|
+
const MAX_PATH_SEGMENTS = 64;
|
|
5
|
+
const MAX_POLICY_SUBJECTS = 1_000_000;
|
|
6
|
+
const SHA256_LENGTH = 32;
|
|
7
|
+
const encoder = new TextEncoder();
|
|
8
|
+
export const VIEW_ACCESS_POLICY_DENY_ROOT = Object.freeze({
|
|
9
|
+
id: VIEW_ACCESS_POLICY_ROOT_SUBJECT_ID,
|
|
10
|
+
kind: "root",
|
|
11
|
+
address: Object.freeze({ kind: "root" }),
|
|
12
|
+
structuralParentId: null,
|
|
13
|
+
mode: "deny",
|
|
14
|
+
});
|
|
15
|
+
export function derivedGraphViewAccessPolicy(snapshot) {
|
|
16
|
+
validateViewAccessPolicy(snapshot);
|
|
17
|
+
return Object.freeze({
|
|
18
|
+
schemaVersion: 3,
|
|
19
|
+
derivation: "graph-v5",
|
|
20
|
+
policySha256: Object.freeze([...snapshot.policySha256]),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const VIEW_ACCESS_MODES = new Set([
|
|
24
|
+
"inherit",
|
|
25
|
+
"deny",
|
|
26
|
+
"read",
|
|
27
|
+
"read_write",
|
|
28
|
+
]);
|
|
29
|
+
const EFFECTIVE_VIEW_ACCESS_MODES = new Set([
|
|
30
|
+
"deny",
|
|
31
|
+
"read",
|
|
32
|
+
"read_write",
|
|
33
|
+
]);
|
|
34
|
+
const SUBJECT_KINDS = new Set([
|
|
35
|
+
"root",
|
|
36
|
+
"node",
|
|
37
|
+
"attr",
|
|
38
|
+
"edge",
|
|
39
|
+
]);
|
|
40
|
+
function fail(code) {
|
|
41
|
+
throw new Error(`view_access_policy.${code}`);
|
|
42
|
+
}
|
|
43
|
+
function assertExactKeys(value, expected, field) {
|
|
44
|
+
const actual = Object.keys(value).sort();
|
|
45
|
+
const canonicalExpected = [...expected].sort();
|
|
46
|
+
if (actual.length !== canonicalExpected.length
|
|
47
|
+
|| actual.some((key, index) => key !== canonicalExpected[index])) {
|
|
48
|
+
fail(`${field}_shape_invalid`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function assertIdentifier(value, field) {
|
|
52
|
+
if (typeof value !== "string"
|
|
53
|
+
|| value.length === 0
|
|
54
|
+
|| value.includes("\0")
|
|
55
|
+
|| encoder.encode(value).byteLength > MAX_IDENTIFIER_BYTES) {
|
|
56
|
+
fail(`${field}_invalid`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function assertMode(value, field) {
|
|
60
|
+
if (typeof value !== "string" || !VIEW_ACCESS_MODES.has(value)) {
|
|
61
|
+
fail(`${field}_invalid`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function assertEffectiveMode(value, field) {
|
|
65
|
+
if (typeof value !== "string"
|
|
66
|
+
|| !EFFECTIVE_VIEW_ACCESS_MODES.has(value)) {
|
|
67
|
+
fail(`${field}_invalid`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function assertLeafPath(value, field) {
|
|
71
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > MAX_PATH_SEGMENTS) {
|
|
72
|
+
fail(`${field}_invalid`);
|
|
73
|
+
}
|
|
74
|
+
for (const segment of value)
|
|
75
|
+
assertIdentifier(segment, `${field}_segment`);
|
|
76
|
+
}
|
|
77
|
+
function assertAttributeOwner(value, field) {
|
|
78
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
79
|
+
fail(`${field}_invalid`);
|
|
80
|
+
}
|
|
81
|
+
const owner = value;
|
|
82
|
+
if (owner.kind === "node") {
|
|
83
|
+
assertExactKeys(value, ["kind", "nodeId"], field);
|
|
84
|
+
assertIdentifier(owner.nodeId, `${field}_node_id`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (owner.kind === "edge") {
|
|
88
|
+
assertExactKeys(value, ["kind", "sourceNodeId", "destinationNodeId", "relationKind"], field);
|
|
89
|
+
assertIdentifier(owner.sourceNodeId, `${field}_source_node_id`);
|
|
90
|
+
assertIdentifier(owner.destinationNodeId, `${field}_destination_node_id`);
|
|
91
|
+
assertIdentifier(owner.relationKind, `${field}_relation_kind`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
fail(`${field}_kind_invalid`);
|
|
95
|
+
}
|
|
96
|
+
function assertPolicyAddress(value, field) {
|
|
97
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
98
|
+
fail(`${field}_invalid`);
|
|
99
|
+
}
|
|
100
|
+
const address = value;
|
|
101
|
+
switch (address.kind) {
|
|
102
|
+
case "root":
|
|
103
|
+
assertExactKeys(value, ["kind"], field);
|
|
104
|
+
return;
|
|
105
|
+
case "node":
|
|
106
|
+
assertExactKeys(value, ["kind", "nodeId"], field);
|
|
107
|
+
assertIdentifier(address.nodeId, `${field}_node_id`);
|
|
108
|
+
return;
|
|
109
|
+
case "attr":
|
|
110
|
+
assertExactKeys(value, ["kind", "owner", "path"], field);
|
|
111
|
+
assertAttributeOwner(address.owner, `${field}_owner`);
|
|
112
|
+
assertLeafPath(address.path, `${field}_path`);
|
|
113
|
+
return;
|
|
114
|
+
case "edge":
|
|
115
|
+
assertExactKeys(value, ["kind", "sourceNodeId", "destinationNodeId", "relationKind"], field);
|
|
116
|
+
assertIdentifier(address.sourceNodeId, `${field}_source_node_id`);
|
|
117
|
+
assertIdentifier(address.destinationNodeId, `${field}_destination_node_id`);
|
|
118
|
+
assertIdentifier(address.relationKind, `${field}_relation_kind`);
|
|
119
|
+
return;
|
|
120
|
+
default:
|
|
121
|
+
fail(`${field}_kind_invalid`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function assertPolicyTarget(value, field) {
|
|
125
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
126
|
+
fail(`${field}_invalid`);
|
|
127
|
+
}
|
|
128
|
+
const target = value;
|
|
129
|
+
if (value.kind === "root")
|
|
130
|
+
fail(`${field}_root_forbidden`);
|
|
131
|
+
if (target.kind === "vector") {
|
|
132
|
+
assertExactKeys(value, ["kind", "nodeId", "vectorSpace"], field);
|
|
133
|
+
assertIdentifier(target.nodeId, `${field}_node_id`);
|
|
134
|
+
assertIdentifier(target.vectorSpace, `${field}_vector_space`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
assertPolicyAddress(value, field);
|
|
138
|
+
}
|
|
139
|
+
function assertTarget(value, field) {
|
|
140
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
141
|
+
fail(`${field}_invalid`);
|
|
142
|
+
}
|
|
143
|
+
const target = value;
|
|
144
|
+
switch (target.kind) {
|
|
145
|
+
case "node":
|
|
146
|
+
assertIdentifier(target.nodeId, `${field}_node_id`);
|
|
147
|
+
return;
|
|
148
|
+
case "attribute":
|
|
149
|
+
if (target.ownerKind !== "node" && target.ownerKind !== "edge") {
|
|
150
|
+
fail(`${field}_owner_kind_invalid`);
|
|
151
|
+
}
|
|
152
|
+
assertIdentifier(target.ownerId, `${field}_owner_id`);
|
|
153
|
+
assertLeafPath(target.path, `${field}_path`);
|
|
154
|
+
return;
|
|
155
|
+
case "edge":
|
|
156
|
+
assertIdentifier(target.edgeId, `${field}_edge_id`);
|
|
157
|
+
assertIdentifier(target.sourceNodeId, `${field}_source_node_id`);
|
|
158
|
+
assertIdentifier(target.destinationNodeId, `${field}_destination_node_id`);
|
|
159
|
+
return;
|
|
160
|
+
case "vector":
|
|
161
|
+
assertIdentifier(target.nodeId, `${field}_node_id`);
|
|
162
|
+
assertIdentifier(target.vectorSpace, `${field}_vector_space`);
|
|
163
|
+
return;
|
|
164
|
+
default:
|
|
165
|
+
fail(`${field}_kind_invalid`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function assertSubject(value, index) {
|
|
169
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
170
|
+
fail(`subjects_${index}_invalid`);
|
|
171
|
+
}
|
|
172
|
+
const subject = value;
|
|
173
|
+
assertExactKeys(value, ["id", "kind", "address", "structuralParentId", "mode"], `subjects_${index}`);
|
|
174
|
+
assertIdentifier(subject.id, `subjects_${index}_id`);
|
|
175
|
+
if (typeof subject.kind !== "string" || !SUBJECT_KINDS.has(subject.kind)) {
|
|
176
|
+
fail(`subjects_${index}_kind_invalid`);
|
|
177
|
+
}
|
|
178
|
+
assertPolicyAddress(subject.address, `subjects_${index}_address`);
|
|
179
|
+
if (subject.kind !== subject.address.kind)
|
|
180
|
+
fail(`subjects_${index}_kind_address_mismatch`);
|
|
181
|
+
if (subject.id !== deriveViewAccessPolicySubjectId(subject.address)) {
|
|
182
|
+
fail(`subjects_${index}_id_address_mismatch`);
|
|
183
|
+
}
|
|
184
|
+
if (subject.structuralParentId !== null) {
|
|
185
|
+
assertIdentifier(subject.structuralParentId, `subjects_${index}_structural_parent_id`);
|
|
186
|
+
}
|
|
187
|
+
assertMode(subject.mode, `subjects_${index}_mode`);
|
|
188
|
+
}
|
|
189
|
+
function indexAndValidateSubjects(subjects, requireCanonicalOrder) {
|
|
190
|
+
if (!Array.isArray(subjects))
|
|
191
|
+
fail("subjects_invalid");
|
|
192
|
+
if (subjects.length > MAX_POLICY_SUBJECTS)
|
|
193
|
+
fail("subject_count_exceeded");
|
|
194
|
+
const byId = new Map();
|
|
195
|
+
let rootCount = 0;
|
|
196
|
+
let previousId = null;
|
|
197
|
+
for (let index = 0; index < subjects.length; index += 1) {
|
|
198
|
+
const subject = subjects[index];
|
|
199
|
+
assertSubject(subject, index);
|
|
200
|
+
if (byId.has(subject.id))
|
|
201
|
+
fail("subject_id_duplicate");
|
|
202
|
+
// assertSubject 已把非 root id 收敛为 `view_subject_<sha256 hex>`;两种合法 id 都是 ASCII,
|
|
203
|
+
// 因此代码单元序与 UTF-8 字节序相同。这里不要推广到任意 Unicode address。
|
|
204
|
+
if (requireCanonicalOrder && previousId !== null && previousId >= subject.id) {
|
|
205
|
+
fail("subjects_not_sorted_unique");
|
|
206
|
+
}
|
|
207
|
+
previousId = subject.id;
|
|
208
|
+
if (subject.kind === "root") {
|
|
209
|
+
rootCount += 1;
|
|
210
|
+
if (subject.id !== VIEW_ACCESS_POLICY_ROOT_SUBJECT_ID
|
|
211
|
+
|| subject.address.kind !== "root"
|
|
212
|
+
|| subject.structuralParentId !== null
|
|
213
|
+
|| subject.mode !== "deny") {
|
|
214
|
+
fail("root_not_fixed_deny");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else if (subject.structuralParentId === null) {
|
|
218
|
+
fail("non_root_structural_parent_missing");
|
|
219
|
+
}
|
|
220
|
+
byId.set(subject.id, subject);
|
|
221
|
+
}
|
|
222
|
+
if (rootCount !== 1)
|
|
223
|
+
fail("root_count_invalid");
|
|
224
|
+
for (const subject of byId.values()) {
|
|
225
|
+
if (subject.kind === "root")
|
|
226
|
+
continue;
|
|
227
|
+
if (subject.structuralParentId === null)
|
|
228
|
+
fail("non_root_structural_parent_missing");
|
|
229
|
+
const parent = byId.get(subject.structuralParentId);
|
|
230
|
+
if (parent === undefined)
|
|
231
|
+
fail("structural_parent_missing");
|
|
232
|
+
switch (subject.address.kind) {
|
|
233
|
+
case "node":
|
|
234
|
+
if (parent.kind !== "root" && parent.kind !== "node") {
|
|
235
|
+
fail("node_structural_parent_kind_invalid");
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
238
|
+
case "edge":
|
|
239
|
+
if (parent.id !== VIEW_ACCESS_POLICY_ROOT_SUBJECT_ID) {
|
|
240
|
+
fail("edge_structural_parent_invalid");
|
|
241
|
+
}
|
|
242
|
+
if (!byId.has(deriveViewAccessPolicySubjectId({
|
|
243
|
+
kind: "node",
|
|
244
|
+
nodeId: subject.address.sourceNodeId,
|
|
245
|
+
}))
|
|
246
|
+
|| !byId.has(deriveViewAccessPolicySubjectId({
|
|
247
|
+
kind: "node",
|
|
248
|
+
nodeId: subject.address.destinationNodeId,
|
|
249
|
+
}))) {
|
|
250
|
+
fail("edge_endpoint_subject_missing");
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
253
|
+
case "attr": {
|
|
254
|
+
const ownerAddress = subject.address.owner.kind === "node"
|
|
255
|
+
? { kind: "node", nodeId: subject.address.owner.nodeId }
|
|
256
|
+
: {
|
|
257
|
+
kind: "edge",
|
|
258
|
+
sourceNodeId: subject.address.owner.sourceNodeId,
|
|
259
|
+
destinationNodeId: subject.address.owner.destinationNodeId,
|
|
260
|
+
relationKind: subject.address.owner.relationKind,
|
|
261
|
+
};
|
|
262
|
+
const ownerSubjectId = deriveViewAccessPolicySubjectId(ownerAddress);
|
|
263
|
+
const ownerSubject = byId.get(ownerSubjectId);
|
|
264
|
+
if (ownerSubject === undefined)
|
|
265
|
+
fail("attribute_owner_subject_missing");
|
|
266
|
+
if (parent.id === ownerSubjectId)
|
|
267
|
+
break;
|
|
268
|
+
if (parent.address.kind !== "attr")
|
|
269
|
+
fail("attribute_structural_parent_invalid");
|
|
270
|
+
const parentOwnerAddress = parent.address.owner.kind === "node"
|
|
271
|
+
? { kind: "node", nodeId: parent.address.owner.nodeId }
|
|
272
|
+
: {
|
|
273
|
+
kind: "edge",
|
|
274
|
+
sourceNodeId: parent.address.owner.sourceNodeId,
|
|
275
|
+
destinationNodeId: parent.address.owner.destinationNodeId,
|
|
276
|
+
relationKind: parent.address.owner.relationKind,
|
|
277
|
+
};
|
|
278
|
+
if (deriveViewAccessPolicySubjectId(parentOwnerAddress) !== ownerSubjectId) {
|
|
279
|
+
fail("attribute_structural_parent_owner_mismatch");
|
|
280
|
+
}
|
|
281
|
+
const childPath = subject.address.path;
|
|
282
|
+
if (parent.address.path.length >= childPath.length
|
|
283
|
+
|| !parent.address.path.every((segment, index) => segment === childPath[index])) {
|
|
284
|
+
fail("attribute_structural_parent_path_invalid");
|
|
285
|
+
}
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
case "root":
|
|
289
|
+
fail("root_structural_parent_forbidden");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const fullyVisited = new Set();
|
|
293
|
+
for (const subject of byId.values()) {
|
|
294
|
+
if (fullyVisited.has(subject.id))
|
|
295
|
+
continue;
|
|
296
|
+
const currentChain = new Set();
|
|
297
|
+
let current = subject;
|
|
298
|
+
while (current !== undefined && !fullyVisited.has(current.id)) {
|
|
299
|
+
if (currentChain.has(current.id))
|
|
300
|
+
fail("structural_parent_cycle");
|
|
301
|
+
currentChain.add(current.id);
|
|
302
|
+
if (current.structuralParentId === null) {
|
|
303
|
+
if (current.id !== VIEW_ACCESS_POLICY_ROOT_SUBJECT_ID)
|
|
304
|
+
fail("chain_not_rooted");
|
|
305
|
+
current = undefined;
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
current = byId.get(current.structuralParentId);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
for (const id of currentChain)
|
|
312
|
+
fullyVisited.add(id);
|
|
313
|
+
}
|
|
314
|
+
return byId;
|
|
315
|
+
}
|
|
316
|
+
const CANONICAL_WRITER_CHUNK_BYTES = 16 * 1024;
|
|
317
|
+
const CANONICAL_ADDRESS_CHUNK_BYTES = 256;
|
|
318
|
+
class CanonicalWriter {
|
|
319
|
+
#chunks;
|
|
320
|
+
#emit;
|
|
321
|
+
#buffer;
|
|
322
|
+
#used = 0;
|
|
323
|
+
#length = 0;
|
|
324
|
+
#finished = false;
|
|
325
|
+
constructor(emit, chunkBytes = CANONICAL_WRITER_CHUNK_BYTES) {
|
|
326
|
+
this.#emit = emit ?? null;
|
|
327
|
+
this.#chunks = emit === undefined ? [] : null;
|
|
328
|
+
this.#buffer = new Uint8Array(chunkBytes);
|
|
329
|
+
}
|
|
330
|
+
#assertWritable() {
|
|
331
|
+
if (this.#finished)
|
|
332
|
+
fail("canonical_writer_finished");
|
|
333
|
+
}
|
|
334
|
+
#flush() {
|
|
335
|
+
if (this.#used === 0)
|
|
336
|
+
return;
|
|
337
|
+
const bytes = this.#buffer.subarray(0, this.#used);
|
|
338
|
+
if (this.#emit === null)
|
|
339
|
+
this.#chunks.push(bytes.slice());
|
|
340
|
+
else
|
|
341
|
+
this.#emit(bytes);
|
|
342
|
+
this.#used = 0;
|
|
343
|
+
}
|
|
344
|
+
u8(value) {
|
|
345
|
+
this.#assertWritable();
|
|
346
|
+
if (this.#used === this.#buffer.length)
|
|
347
|
+
this.#flush();
|
|
348
|
+
this.#buffer[this.#used] = value & 0xff;
|
|
349
|
+
this.#used += 1;
|
|
350
|
+
this.#length += 1;
|
|
351
|
+
}
|
|
352
|
+
u16(value) {
|
|
353
|
+
this.u8((value >>> 8) & 0xff);
|
|
354
|
+
this.u8(value & 0xff);
|
|
355
|
+
}
|
|
356
|
+
u32(value) {
|
|
357
|
+
this.u8((value >>> 24) & 0xff);
|
|
358
|
+
this.u8((value >>> 16) & 0xff);
|
|
359
|
+
this.u8((value >>> 8) & 0xff);
|
|
360
|
+
this.u8(value & 0xff);
|
|
361
|
+
}
|
|
362
|
+
raw(value) {
|
|
363
|
+
this.#assertWritable();
|
|
364
|
+
let offset = 0;
|
|
365
|
+
while (offset < value.length) {
|
|
366
|
+
if (this.#used === this.#buffer.length)
|
|
367
|
+
this.#flush();
|
|
368
|
+
const count = Math.min(value.length - offset, this.#buffer.length - this.#used);
|
|
369
|
+
this.#buffer.set(value.subarray(offset, offset + count), this.#used);
|
|
370
|
+
this.#used += count;
|
|
371
|
+
this.#length += count;
|
|
372
|
+
offset += count;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
string(value) {
|
|
376
|
+
const bytes = encoder.encode(value);
|
|
377
|
+
this.u32(bytes.length);
|
|
378
|
+
this.raw(bytes);
|
|
379
|
+
}
|
|
380
|
+
finish() {
|
|
381
|
+
this.#assertWritable();
|
|
382
|
+
if (this.#emit !== null)
|
|
383
|
+
fail("canonical_writer_materialize_stream");
|
|
384
|
+
this.#flush();
|
|
385
|
+
this.#finished = true;
|
|
386
|
+
const output = new Uint8Array(this.#length);
|
|
387
|
+
let offset = 0;
|
|
388
|
+
for (const chunk of this.#chunks) {
|
|
389
|
+
output.set(chunk, offset);
|
|
390
|
+
offset += chunk.length;
|
|
391
|
+
}
|
|
392
|
+
return output;
|
|
393
|
+
}
|
|
394
|
+
finishStreaming() {
|
|
395
|
+
this.#assertWritable();
|
|
396
|
+
if (this.#emit === null)
|
|
397
|
+
fail("canonical_writer_stream_missing");
|
|
398
|
+
this.#flush();
|
|
399
|
+
this.#finished = true;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function canonicalAddressBytes(address) {
|
|
403
|
+
assertPolicyAddress(address, "address");
|
|
404
|
+
// 单 address 上限受 identifier/path cap 约束,通常远小于整份 policy。
|
|
405
|
+
const writer = new CanonicalWriter(undefined, CANONICAL_ADDRESS_CHUNK_BYTES);
|
|
406
|
+
switch (address.kind) {
|
|
407
|
+
case "root":
|
|
408
|
+
writer.u8(1);
|
|
409
|
+
break;
|
|
410
|
+
case "node":
|
|
411
|
+
writer.u8(2);
|
|
412
|
+
writer.string(address.nodeId);
|
|
413
|
+
break;
|
|
414
|
+
case "attr":
|
|
415
|
+
writer.u8(3);
|
|
416
|
+
if (address.owner.kind === "node") {
|
|
417
|
+
writer.u8(1);
|
|
418
|
+
writer.string(address.owner.nodeId);
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
writer.u8(2);
|
|
422
|
+
writer.string(address.owner.sourceNodeId);
|
|
423
|
+
writer.string(address.owner.destinationNodeId);
|
|
424
|
+
writer.string(address.owner.relationKind);
|
|
425
|
+
}
|
|
426
|
+
writer.u16(address.path.length);
|
|
427
|
+
for (const segment of address.path)
|
|
428
|
+
writer.string(segment);
|
|
429
|
+
break;
|
|
430
|
+
case "edge":
|
|
431
|
+
writer.u8(4);
|
|
432
|
+
writer.string(address.sourceNodeId);
|
|
433
|
+
writer.string(address.destinationNodeId);
|
|
434
|
+
writer.string(address.relationKind);
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
return writer.finish();
|
|
438
|
+
}
|
|
439
|
+
function hashParts(domain, parts) {
|
|
440
|
+
const hasher = new Sha256Incremental();
|
|
441
|
+
hasher.update(encoder.encode(domain));
|
|
442
|
+
const lengthBytes = new Uint8Array(8);
|
|
443
|
+
for (const part of parts) {
|
|
444
|
+
if (!Number.isSafeInteger(part.length) || part.length < 0)
|
|
445
|
+
fail("canonical_length_invalid");
|
|
446
|
+
let remaining = BigInt(part.length);
|
|
447
|
+
for (let index = 0; index < lengthBytes.length; index += 1) {
|
|
448
|
+
lengthBytes[index] = Number(remaining & 0xffn);
|
|
449
|
+
remaining >>= 8n;
|
|
450
|
+
}
|
|
451
|
+
hasher.update(lengthBytes);
|
|
452
|
+
hasher.update(part);
|
|
453
|
+
}
|
|
454
|
+
return hasher.digest();
|
|
455
|
+
}
|
|
456
|
+
function hexDigest(bytes) {
|
|
457
|
+
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
458
|
+
}
|
|
459
|
+
/** Canonical Ladybug relation identity shared with the Rust store. */
|
|
460
|
+
export function canonicalLadybugRelationId(sourceNodeId, destinationNodeId, relationKind) {
|
|
461
|
+
assertIdentifier(sourceNodeId, "relation_source_node_id");
|
|
462
|
+
assertIdentifier(destinationNodeId, "relation_destination_node_id");
|
|
463
|
+
assertIdentifier(relationKind, "relation_kind");
|
|
464
|
+
return `rel_${hexDigest(hashParts("ladybug-relation-id-v1", [
|
|
465
|
+
encoder.encode(sourceNodeId),
|
|
466
|
+
encoder.encode(destinationNodeId),
|
|
467
|
+
encoder.encode(relationKind),
|
|
468
|
+
]))}`;
|
|
469
|
+
}
|
|
470
|
+
/** Domain-separated stable identity of one typed policy address. */
|
|
471
|
+
export function deriveViewAccessPolicySubjectId(address) {
|
|
472
|
+
assertPolicyAddress(address, "subject_address");
|
|
473
|
+
if (address.kind === "root")
|
|
474
|
+
return VIEW_ACCESS_POLICY_ROOT_SUBJECT_ID;
|
|
475
|
+
return `view_subject_${hexDigest(hashParts("ladybug-view-access-subject-id-v2", [canonicalAddressBytes(address)]))}`;
|
|
476
|
+
}
|
|
477
|
+
/** Constructs a bound non-root subject; the root is the exported fixed constant. */
|
|
478
|
+
export function createViewAccessPolicySubject(address, structuralParentId, mode) {
|
|
479
|
+
assertPolicyAddress(address, "subject_address");
|
|
480
|
+
if (address.kind === "root") {
|
|
481
|
+
fail("root_requires_fixed_constructor");
|
|
482
|
+
}
|
|
483
|
+
assertIdentifier(structuralParentId, "structural_parent_id");
|
|
484
|
+
assertMode(mode, "mode");
|
|
485
|
+
const cloned = clonePolicyAddress(address);
|
|
486
|
+
return {
|
|
487
|
+
id: deriveViewAccessPolicySubjectId(cloned),
|
|
488
|
+
kind: cloned.kind,
|
|
489
|
+
address: cloned,
|
|
490
|
+
structuralParentId,
|
|
491
|
+
mode,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Reduces a runtime target to its policy address. Vector spaces cannot carry
|
|
496
|
+
* policy overrides and always resolve through their owning node.
|
|
497
|
+
*/
|
|
498
|
+
export function viewAccessPolicyAddressForTarget(target) {
|
|
499
|
+
assertPolicyTarget(target, "target");
|
|
500
|
+
if (target.kind === "vector")
|
|
501
|
+
return { kind: "node", nodeId: target.nodeId };
|
|
502
|
+
return clonePolicyAddress(target);
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Stable target-to-subject mapping used by every query and mutation gate.
|
|
506
|
+
* Vector-space names are validated but deliberately do not influence the id:
|
|
507
|
+
* every vector is governed by the policy subject of its owning node.
|
|
508
|
+
*/
|
|
509
|
+
export function deriveViewAccessPolicySubjectIdForTarget(target) {
|
|
510
|
+
return deriveViewAccessPolicySubjectId(viewAccessPolicyAddressForTarget(target));
|
|
511
|
+
}
|
|
512
|
+
function clonePolicyAddress(address) {
|
|
513
|
+
switch (address.kind) {
|
|
514
|
+
case "root":
|
|
515
|
+
return { kind: "root" };
|
|
516
|
+
case "node":
|
|
517
|
+
return { kind: "node", nodeId: address.nodeId };
|
|
518
|
+
case "edge":
|
|
519
|
+
return {
|
|
520
|
+
kind: "edge",
|
|
521
|
+
sourceNodeId: address.sourceNodeId,
|
|
522
|
+
destinationNodeId: address.destinationNodeId,
|
|
523
|
+
relationKind: address.relationKind,
|
|
524
|
+
};
|
|
525
|
+
case "attr":
|
|
526
|
+
return {
|
|
527
|
+
kind: "attr",
|
|
528
|
+
owner: address.owner.kind === "node"
|
|
529
|
+
? { kind: "node", nodeId: address.owner.nodeId }
|
|
530
|
+
: {
|
|
531
|
+
kind: "edge",
|
|
532
|
+
sourceNodeId: address.owner.sourceNodeId,
|
|
533
|
+
destinationNodeId: address.owner.destinationNodeId,
|
|
534
|
+
relationKind: address.owner.relationKind,
|
|
535
|
+
},
|
|
536
|
+
path: [...address.path],
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function canonicalPolicyBytes(subjects, writer) {
|
|
541
|
+
writer.raw(encoder.encode("ladybug-view-access-policy\0"));
|
|
542
|
+
writer.u16(VIEW_ACCESS_POLICY_SCHEMA_VERSION);
|
|
543
|
+
writer.u32(subjects.length);
|
|
544
|
+
for (const subject of subjects) {
|
|
545
|
+
writer.string(subject.id);
|
|
546
|
+
writer.u8({ root: 1, node: 2, attr: 3, edge: 4 }[subject.kind]);
|
|
547
|
+
writer.raw(canonicalAddressBytes(subject.address));
|
|
548
|
+
if (subject.structuralParentId === null) {
|
|
549
|
+
writer.u8(0);
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
writer.u8(1);
|
|
553
|
+
writer.string(subject.structuralParentId);
|
|
554
|
+
}
|
|
555
|
+
writer.u8({ inherit: 0, deny: 1, read: 2, read_write: 3 }[subject.mode]);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function canonicalPolicySha256(subjects) {
|
|
559
|
+
const hasher = new Sha256Incremental();
|
|
560
|
+
const writer = new CanonicalWriter((bytes) => hasher.update(bytes));
|
|
561
|
+
canonicalPolicyBytes(subjects, writer);
|
|
562
|
+
writer.finishStreaming();
|
|
563
|
+
return hasher.digest();
|
|
564
|
+
}
|
|
565
|
+
/** Sorts, validates and seals one canonical fail-closed policy snapshot. */
|
|
566
|
+
export function sealViewAccessPolicy(subjects) {
|
|
567
|
+
if (!Array.isArray(subjects))
|
|
568
|
+
fail("subjects_invalid");
|
|
569
|
+
indexAndValidateSubjects(subjects, false);
|
|
570
|
+
const canonicalSubjects = subjects
|
|
571
|
+
.map((subject) => ({ ...subject, address: clonePolicyAddress(subject.address) }))
|
|
572
|
+
// subject.id 已经过 assertSubject 的派生 identity 检查,只可能是 ASCII root/hex id。
|
|
573
|
+
.sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0);
|
|
574
|
+
indexAndValidateSubjects(canonicalSubjects, true);
|
|
575
|
+
return {
|
|
576
|
+
schemaVersion: VIEW_ACCESS_POLICY_SCHEMA_VERSION,
|
|
577
|
+
subjects: canonicalSubjects,
|
|
578
|
+
policySha256: [...canonicalPolicySha256(canonicalSubjects)],
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
/** Validates canonical ordering, graph invariants and digest. */
|
|
582
|
+
export function validateViewAccessPolicy(snapshot) {
|
|
583
|
+
if (typeof snapshot !== "object" || snapshot === null || Array.isArray(snapshot)) {
|
|
584
|
+
fail("snapshot_invalid");
|
|
585
|
+
}
|
|
586
|
+
assertExactKeys(snapshot, ["schemaVersion", "subjects", "policySha256"], "snapshot");
|
|
587
|
+
if (snapshot.schemaVersion !== VIEW_ACCESS_POLICY_SCHEMA_VERSION) {
|
|
588
|
+
fail("schema_version_unsupported");
|
|
589
|
+
}
|
|
590
|
+
indexAndValidateSubjects(snapshot.subjects, true);
|
|
591
|
+
if (!Array.isArray(snapshot.policySha256)
|
|
592
|
+
|| snapshot.policySha256.length !== SHA256_LENGTH
|
|
593
|
+
|| snapshot.policySha256.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) {
|
|
594
|
+
fail("digest_invalid");
|
|
595
|
+
}
|
|
596
|
+
const expected = canonicalPolicySha256(snapshot.subjects);
|
|
597
|
+
if (!expected.every((octet, index) => octet === snapshot.policySha256[index])) {
|
|
598
|
+
fail("digest_mismatch");
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Resolves the closest explicit mode on the subject's structural parent chain.
|
|
603
|
+
* A chain with only `inherit` modes ends at the sealed fixed-deny root.
|
|
604
|
+
*/
|
|
605
|
+
export function resolveViewAccessMode(subjects, subjectId) {
|
|
606
|
+
assertIdentifier(subjectId, "subject_id");
|
|
607
|
+
const byId = indexAndValidateSubjects(subjects, false);
|
|
608
|
+
let current = byId.get(subjectId);
|
|
609
|
+
if (current === undefined)
|
|
610
|
+
fail("subject_missing");
|
|
611
|
+
while (current !== undefined) {
|
|
612
|
+
if (current.mode !== "inherit") {
|
|
613
|
+
return { mode: current.mode, decidedBySubjectId: current.id };
|
|
614
|
+
}
|
|
615
|
+
if (current.structuralParentId === null)
|
|
616
|
+
fail("resolution_chain_terminated_before_decision");
|
|
617
|
+
current = byId.get(current.structuralParentId);
|
|
618
|
+
}
|
|
619
|
+
fail("resolution_chain_missing");
|
|
620
|
+
}
|
|
621
|
+
export function isViewReadable(mode) {
|
|
622
|
+
assertEffectiveMode(mode, "mode");
|
|
623
|
+
return mode === "read" || mode === "read_write";
|
|
624
|
+
}
|
|
625
|
+
export function isViewWritable(mode) {
|
|
626
|
+
assertEffectiveMode(mode, "mode");
|
|
627
|
+
return mode === "read_write";
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Edge access never inherits through its endpoints. Endpoints are gates only:
|
|
631
|
+
* both must be readable; writing additionally requires the edge itself to be
|
|
632
|
+
* explicitly/effectively `read_write`.
|
|
633
|
+
*/
|
|
634
|
+
export function resolveEdgeViewAccessMode(edgeMode, sourceMode, destinationMode) {
|
|
635
|
+
assertEffectiveMode(edgeMode, "edge_mode");
|
|
636
|
+
assertEffectiveMode(sourceMode, "source_mode");
|
|
637
|
+
assertEffectiveMode(destinationMode, "destination_mode");
|
|
638
|
+
if (!isViewReadable(sourceMode) || !isViewReadable(destinationMode))
|
|
639
|
+
return "deny";
|
|
640
|
+
if (edgeMode === "read_write")
|
|
641
|
+
return "read_write";
|
|
642
|
+
if (edgeMode === "read")
|
|
643
|
+
return "read";
|
|
644
|
+
return "deny";
|
|
645
|
+
}
|
|
646
|
+
function pathsEqual(left, right) {
|
|
647
|
+
return left.length === right.length && left.every((segment, index) => segment === right[index]);
|
|
648
|
+
}
|
|
649
|
+
function targetsEqual(left, right) {
|
|
650
|
+
if (left.kind !== right.kind)
|
|
651
|
+
return false;
|
|
652
|
+
switch (left.kind) {
|
|
653
|
+
case "node":
|
|
654
|
+
return right.kind === "node" && left.nodeId === right.nodeId;
|
|
655
|
+
case "attribute":
|
|
656
|
+
return right.kind === "attribute"
|
|
657
|
+
&& left.ownerKind === right.ownerKind
|
|
658
|
+
&& left.ownerId === right.ownerId
|
|
659
|
+
&& pathsEqual(left.path, right.path);
|
|
660
|
+
case "edge":
|
|
661
|
+
return right.kind === "edge"
|
|
662
|
+
&& left.edgeId === right.edgeId
|
|
663
|
+
&& left.sourceNodeId === right.sourceNodeId
|
|
664
|
+
&& left.destinationNodeId === right.destinationNodeId;
|
|
665
|
+
case "vector":
|
|
666
|
+
return right.kind === "vector"
|
|
667
|
+
&& left.nodeId === right.nodeId
|
|
668
|
+
&& left.vectorSpace === right.vectorSpace;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
function assertWalChange(change, index) {
|
|
672
|
+
if (typeof change !== "object" || change === null || Array.isArray(change)) {
|
|
673
|
+
fail(`canonical_wal_changes_${index}_invalid`);
|
|
674
|
+
}
|
|
675
|
+
const candidate = change;
|
|
676
|
+
assertTarget(candidate.target, `canonical_wal_changes_${index}_target`);
|
|
677
|
+
assertLeafPath(candidate.leafPath, `canonical_wal_changes_${index}_leaf_path`);
|
|
678
|
+
if (candidate.provenance !== "baseline" && candidate.provenance !== "overlay_created") {
|
|
679
|
+
fail(`canonical_wal_changes_${index}_provenance_invalid`);
|
|
680
|
+
}
|
|
681
|
+
if (candidate.target.kind === "attribute" && !pathsEqual(candidate.target.path, candidate.leafPath)) {
|
|
682
|
+
fail("attribute_leaf_path_mismatch");
|
|
683
|
+
}
|
|
684
|
+
if (candidate.target.kind === "vector")
|
|
685
|
+
fail("vector_leaf_change_forbidden");
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Makes a leaf-scoped overlay decision. Canonical WAL visibility is granted
|
|
689
|
+
* only for an exact typed target and changed-leaf path; neither ancestor-prefix
|
|
690
|
+
* matches, a changed sibling nor another owner can make baseline data visible.
|
|
691
|
+
*/
|
|
692
|
+
export function resolveOverlayLeafViewAccessMode(input) {
|
|
693
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
694
|
+
fail("overlay_leaf_input_invalid");
|
|
695
|
+
}
|
|
696
|
+
assertTarget(input.target, "overlay_leaf_target");
|
|
697
|
+
assertLeafPath(input.leafPath, "overlay_leaf_path");
|
|
698
|
+
if (input.target.kind === "attribute" && !pathsEqual(input.target.path, input.leafPath)) {
|
|
699
|
+
fail("attribute_leaf_path_mismatch");
|
|
700
|
+
}
|
|
701
|
+
if (input.target.kind === "vector")
|
|
702
|
+
fail("vector_leaf_change_forbidden");
|
|
703
|
+
if (input.provenance !== "baseline" && input.provenance !== "overlay_created") {
|
|
704
|
+
fail("overlay_leaf_provenance_invalid");
|
|
705
|
+
}
|
|
706
|
+
assertEffectiveMode(input.policyMode, "overlay_leaf_policy_mode");
|
|
707
|
+
if (!Array.isArray(input.canonicalWalChanges))
|
|
708
|
+
fail("canonical_wal_changes_invalid");
|
|
709
|
+
let exactWalChange = false;
|
|
710
|
+
for (let index = 0; index < input.canonicalWalChanges.length; index += 1) {
|
|
711
|
+
const change = input.canonicalWalChanges[index];
|
|
712
|
+
assertWalChange(change, index);
|
|
713
|
+
if (targetsEqual(input.target, change.target) && pathsEqual(input.leafPath, change.leafPath)) {
|
|
714
|
+
exactWalChange = true;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (input.provenance === "overlay_created" || exactWalChange)
|
|
718
|
+
return "read_write";
|
|
719
|
+
return input.policyMode;
|
|
720
|
+
}
|
|
721
|
+
function rotateRight(value, count) {
|
|
722
|
+
return (value >>> count) | (value << (32 - count));
|
|
723
|
+
}
|
|
724
|
+
class Sha256Incremental {
|
|
725
|
+
#words = new Uint32Array(64);
|
|
726
|
+
#hash = new Uint32Array([
|
|
727
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
|
|
728
|
+
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
|
729
|
+
]);
|
|
730
|
+
#block = new Uint8Array(64);
|
|
731
|
+
#blockLength = 0;
|
|
732
|
+
#byteLength = 0n;
|
|
733
|
+
#finished = false;
|
|
734
|
+
#compress(bytes, offset) {
|
|
735
|
+
for (let index = 0; index < 16; index += 1) {
|
|
736
|
+
const at = offset + index * 4;
|
|
737
|
+
this.#words[index] = ((bytes[at] << 24) | (bytes[at + 1] << 16)
|
|
738
|
+
| (bytes[at + 2] << 8) | bytes[at + 3]) >>> 0;
|
|
739
|
+
}
|
|
740
|
+
for (let index = 16; index < 64; index += 1) {
|
|
741
|
+
const a = this.#words[index - 15];
|
|
742
|
+
const b = this.#words[index - 2];
|
|
743
|
+
const s0 = rotateRight(a, 7) ^ rotateRight(a, 18) ^ (a >>> 3);
|
|
744
|
+
const s1 = rotateRight(b, 17) ^ rotateRight(b, 19) ^ (b >>> 10);
|
|
745
|
+
this.#words[index] = (this.#words[index - 16] + s0 + this.#words[index - 7] + s1) >>> 0;
|
|
746
|
+
}
|
|
747
|
+
let [a, b, c, d, e, f, g, h] = this.#hash;
|
|
748
|
+
for (let index = 0; index < 64; index += 1) {
|
|
749
|
+
const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
|
|
750
|
+
const choice = (e & f) ^ (~e & g);
|
|
751
|
+
const t1 = (h + sum1 + choice + SHA256_K[index] + this.#words[index]) >>> 0;
|
|
752
|
+
const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
|
|
753
|
+
const majority = (a & b) ^ (a & c) ^ (b & c);
|
|
754
|
+
const t2 = (sum0 + majority) >>> 0;
|
|
755
|
+
h = g;
|
|
756
|
+
g = f;
|
|
757
|
+
f = e;
|
|
758
|
+
e = (d + t1) >>> 0;
|
|
759
|
+
d = c;
|
|
760
|
+
c = b;
|
|
761
|
+
b = a;
|
|
762
|
+
a = (t1 + t2) >>> 0;
|
|
763
|
+
}
|
|
764
|
+
this.#hash[0] = (this.#hash[0] + a) >>> 0;
|
|
765
|
+
this.#hash[1] = (this.#hash[1] + b) >>> 0;
|
|
766
|
+
this.#hash[2] = (this.#hash[2] + c) >>> 0;
|
|
767
|
+
this.#hash[3] = (this.#hash[3] + d) >>> 0;
|
|
768
|
+
this.#hash[4] = (this.#hash[4] + e) >>> 0;
|
|
769
|
+
this.#hash[5] = (this.#hash[5] + f) >>> 0;
|
|
770
|
+
this.#hash[6] = (this.#hash[6] + g) >>> 0;
|
|
771
|
+
this.#hash[7] = (this.#hash[7] + h) >>> 0;
|
|
772
|
+
}
|
|
773
|
+
update(bytes) {
|
|
774
|
+
if (this.#finished)
|
|
775
|
+
fail("sha256_finished");
|
|
776
|
+
this.#byteLength += BigInt(bytes.length);
|
|
777
|
+
let offset = 0;
|
|
778
|
+
if (this.#blockLength > 0) {
|
|
779
|
+
const count = Math.min(bytes.length, 64 - this.#blockLength);
|
|
780
|
+
this.#block.set(bytes.subarray(0, count), this.#blockLength);
|
|
781
|
+
this.#blockLength += count;
|
|
782
|
+
offset += count;
|
|
783
|
+
if (this.#blockLength === 64) {
|
|
784
|
+
this.#compress(this.#block, 0);
|
|
785
|
+
this.#blockLength = 0;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
while (offset + 64 <= bytes.length) {
|
|
789
|
+
this.#compress(bytes, offset);
|
|
790
|
+
offset += 64;
|
|
791
|
+
}
|
|
792
|
+
if (offset < bytes.length) {
|
|
793
|
+
const remaining = bytes.subarray(offset);
|
|
794
|
+
this.#block.set(remaining, 0);
|
|
795
|
+
this.#blockLength = remaining.length;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
digest() {
|
|
799
|
+
if (this.#finished)
|
|
800
|
+
fail("sha256_finished");
|
|
801
|
+
this.#finished = true;
|
|
802
|
+
const bitLength = this.#byteLength * 8n;
|
|
803
|
+
const used = this.#blockLength;
|
|
804
|
+
this.#block[used] = 0x80;
|
|
805
|
+
this.#block.fill(0, used + 1);
|
|
806
|
+
if (used >= 56) {
|
|
807
|
+
this.#compress(this.#block, 0);
|
|
808
|
+
this.#block.fill(0);
|
|
809
|
+
}
|
|
810
|
+
for (let index = 0; index < 8; index += 1) {
|
|
811
|
+
this.#block[63 - index] = Number((bitLength >> BigInt(index * 8)) & 0xffn);
|
|
812
|
+
}
|
|
813
|
+
this.#compress(this.#block, 0);
|
|
814
|
+
const digest = new Uint8Array(SHA256_LENGTH);
|
|
815
|
+
for (let index = 0; index < this.#hash.length; index += 1) {
|
|
816
|
+
const word = this.#hash[index];
|
|
817
|
+
const at = index * 4;
|
|
818
|
+
digest[at] = (word >>> 24) & 0xff;
|
|
819
|
+
digest[at + 1] = (word >>> 16) & 0xff;
|
|
820
|
+
digest[at + 2] = (word >>> 8) & 0xff;
|
|
821
|
+
digest[at + 3] = word & 0xff;
|
|
822
|
+
}
|
|
823
|
+
return digest;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
const SHA256_K = new Uint32Array([
|
|
827
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
828
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
829
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
830
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
831
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
832
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
833
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
834
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
835
|
+
]);
|