@sentropic/llm-mesh 0.32.4 → 0.34.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.
Files changed (73) hide show
  1. package/dist/enrollment/index.d.ts +4 -0
  2. package/dist/enrollment/index.d.ts.map +1 -1
  3. package/dist/enrollment/index.js +2 -0
  4. package/dist/enrollment/index.js.map +1 -1
  5. package/dist/enrollment/muse-code.d.ts +9 -0
  6. package/dist/enrollment/muse-code.d.ts.map +1 -1
  7. package/dist/enrollment/muse-code.js +22 -1
  8. package/dist/enrollment/muse-code.js.map +1 -1
  9. package/dist/enrollment/seat-onboarding-projections.d.ts +44 -0
  10. package/dist/enrollment/seat-onboarding-projections.d.ts.map +1 -0
  11. package/dist/enrollment/seat-onboarding-projections.js +113 -0
  12. package/dist/enrollment/seat-onboarding-projections.js.map +1 -0
  13. package/dist/enrollment/seat-onboarding.d.ts +161 -0
  14. package/dist/enrollment/seat-onboarding.d.ts.map +1 -0
  15. package/dist/enrollment/seat-onboarding.js +698 -0
  16. package/dist/enrollment/seat-onboarding.js.map +1 -0
  17. package/dist/node/index.d.ts +8 -0
  18. package/dist/node/index.d.ts.map +1 -1
  19. package/dist/node/index.js +4 -0
  20. package/dist/node/index.js.map +1 -1
  21. package/dist/node/seat-onboarding-capture.d.ts +47 -0
  22. package/dist/node/seat-onboarding-capture.d.ts.map +1 -0
  23. package/dist/node/seat-onboarding-capture.js +164 -0
  24. package/dist/node/seat-onboarding-capture.js.map +1 -0
  25. package/dist/node/seat-onboarding-cell-compose.d.ts +25 -0
  26. package/dist/node/seat-onboarding-cell-compose.d.ts.map +1 -0
  27. package/dist/node/seat-onboarding-cell-compose.js +117 -0
  28. package/dist/node/seat-onboarding-cell-compose.js.map +1 -0
  29. package/dist/node/seat-onboarding-erase.d.ts +8 -0
  30. package/dist/node/seat-onboarding-erase.d.ts.map +1 -0
  31. package/dist/node/seat-onboarding-erase.js +31 -0
  32. package/dist/node/seat-onboarding-erase.js.map +1 -0
  33. package/dist/node/seat-onboarding-materialize.d.ts +6 -0
  34. package/dist/node/seat-onboarding-materialize.d.ts.map +1 -0
  35. package/dist/node/seat-onboarding-materialize.js +11 -0
  36. package/dist/node/seat-onboarding-materialize.js.map +1 -0
  37. package/dist/node/seat-onboarding-record.d.ts +11 -0
  38. package/dist/node/seat-onboarding-record.d.ts.map +1 -0
  39. package/dist/node/seat-onboarding-record.js +86 -0
  40. package/dist/node/seat-onboarding-record.js.map +1 -0
  41. package/dist/node/seat-onboarding-stage1.d.ts +11 -0
  42. package/dist/node/seat-onboarding-stage1.d.ts.map +1 -0
  43. package/dist/node/seat-onboarding-stage1.js +121 -0
  44. package/dist/node/seat-onboarding-stage1.js.map +1 -0
  45. package/dist/node/seat-onboarding-stage2.d.ts +10 -0
  46. package/dist/node/seat-onboarding-stage2.d.ts.map +1 -0
  47. package/dist/node/seat-onboarding-stage2.js +110 -0
  48. package/dist/node/seat-onboarding-stage2.js.map +1 -0
  49. package/dist/node/seat-onboarding-types.d.ts +106 -0
  50. package/dist/node/seat-onboarding-types.d.ts.map +1 -0
  51. package/dist/node/seat-onboarding-types.js +6 -0
  52. package/dist/node/seat-onboarding-types.js.map +1 -0
  53. package/dist/node/seat-stage-write.d.ts +25 -0
  54. package/dist/node/seat-stage-write.d.ts.map +1 -0
  55. package/dist/node/seat-stage-write.js +93 -0
  56. package/dist/node/seat-stage-write.js.map +1 -0
  57. package/dist/service/custodian-refresh.d.ts +52 -0
  58. package/dist/service/custodian-refresh.d.ts.map +1 -0
  59. package/dist/service/custodian-refresh.js +51 -0
  60. package/dist/service/custodian-refresh.js.map +1 -0
  61. package/dist/service/facade.d.ts +28 -0
  62. package/dist/service/facade.d.ts.map +1 -1
  63. package/dist/service/facade.js +18 -1
  64. package/dist/service/facade.js.map +1 -1
  65. package/dist/service/local-account-transport-service.d.ts +127 -2
  66. package/dist/service/local-account-transport-service.d.ts.map +1 -1
  67. package/dist/service/local-account-transport-service.js +540 -10
  68. package/dist/service/local-account-transport-service.js.map +1 -1
  69. package/dist/service/reauth-alert.d.ts +29 -0
  70. package/dist/service/reauth-alert.d.ts.map +1 -0
  71. package/dist/service/reauth-alert.js +2 -0
  72. package/dist/service/reauth-alert.js.map +1 -0
  73. package/package.json +1 -1
@@ -0,0 +1,698 @@
1
+ // @sentropic/llm-mesh/enrollment/seat-onboarding.ts
2
+ //
3
+ // SeatOnboardingV1: the sibling onboarding block that travels inside the
4
+ // serialized seat envelope next to the frozen SeatBundleV1. Contract and
5
+ // validators only — this module performs no home, keyring, network or
6
+ // provider access (same rule as seat-bundle.ts). Capture normalizes and
7
+ // re-serializes an allowlisted subset; transport accepts canonical bytes
8
+ // only. Every refusal is fail-closed at the stage that raises it.
9
+ import { createHash } from 'node:crypto';
10
+ import { SEAT_BUNDLE_MAX_BYTES, classifyTransition, validateSeatBundle, } from './seat-bundle.js';
11
+ import { AgyMappingError, translateAgySeatToApplication } from './agy-cli-import.js';
12
+ export const SEAT_ONBOARDING_SCHEMA_VERSION = 1;
13
+ /** Whole onboarding block cap, serialized; sidecar precedent (4 KiB). */
14
+ export const SEAT_ONBOARDING_MAX_BYTES = 4096;
15
+ export class SeatOnboardingError extends Error {
16
+ code;
17
+ constructor(code, message) {
18
+ super(message);
19
+ this.name = 'SeatOnboardingError';
20
+ this.code = code;
21
+ }
22
+ }
23
+ /** Bench-measured auth-file allowlist, seat-directory-relative storage names. */
24
+ export const SEAT_ENVELOPE_AUTH_FILES = {
25
+ agy: ['.gemini/antigravity-cli/antigravity-oauth-token'],
26
+ claude: ['.claude/.credentials.json', '.claude.json'],
27
+ codex: ['.codex/auth.json'],
28
+ muse: ['.config/muse/auth.json'],
29
+ vibe: ['.vibe/MISTRAL_API_KEY'],
30
+ };
31
+ /** Decoded-byte cap per transported onboarding file. */
32
+ export const SEAT_ONBOARDING_FILE_CAPS = {
33
+ 'agy-jetski': 1024,
34
+ 'claude-json': 1024,
35
+ 'muse-trust': 1024,
36
+ 'codex-config': 2048,
37
+ };
38
+ const DEST_FORMAT = {
39
+ 'agy-jetski': 'agy-jetski-pbtxt-v1',
40
+ 'claude-json': 'claude-json-projection-v1',
41
+ 'muse-trust': 'muse-trust-json-v1',
42
+ 'codex-config': 'codex-config-projection-v1',
43
+ };
44
+ /** v1 allowlist entry per CLI; vibe has none until a gate is measured. */
45
+ const CLI_DEST = {
46
+ agy: 'agy-jetski',
47
+ claude: 'claude-json',
48
+ muse: 'muse-trust',
49
+ codex: 'codex-config',
50
+ vibe: null,
51
+ };
52
+ /** Reviewed full completed-step set per pinned agy version (measured 1.4.12). */
53
+ export const AGY_REQUIRED_COMPLETED_STEPS = {
54
+ '1.4.12': [
55
+ 'POST_ONBOARDING_STEP_TYPE_MANAGER_WELCOME',
56
+ 'POST_ONBOARDING_STEP_TYPE_USAGE_MODE',
57
+ 'POST_ONBOARDING_STEP_TYPE_AGENT_CONFIGURATION',
58
+ 'POST_ONBOARDING_STEP_TYPE_ADD_WORKSPACE',
59
+ ],
60
+ };
61
+ /** Reviewed muse trust.json schema versions; starts as a singleton. */
62
+ export const MUSE_TRUST_KNOWN_SCHEMA_VERSIONS = [1];
63
+ /** Reviewed codex approval policies for v1 (sufficiency unverified until the L4 probe). */
64
+ export const CODEX_APPROVAL_POLICY_VALUES = [
65
+ 'untrusted',
66
+ 'on-failure',
67
+ 'on-request',
68
+ 'never',
69
+ ];
70
+ const CLIS = ['agy', 'codex', 'vibe', 'muse', 'claude'];
71
+ const GATED_CLIS = ['agy', 'claude', 'muse', 'codex'];
72
+ const SYNTHESIS_CLIS = {
73
+ 'claude-workspace-trust': 'claude',
74
+ 'muse-workspace-trust': 'muse',
75
+ 'codex-project-trust': 'codex',
76
+ };
77
+ /**
78
+ * The single gate fact every downstream stage uses: a present block (any
79
+ * CLI) and vibe-absent are eligible; agy/claude/muse/codex absent close the
80
+ * gate with the launch-gate code `onboarding-required`.
81
+ */
82
+ export function seatLaunchGate(cli, onboardingStatus) {
83
+ if (onboardingStatus === 'present')
84
+ return { eligible: true };
85
+ if (!GATED_CLIS.includes(cli))
86
+ return { eligible: true };
87
+ return { eligible: false, code: 'onboarding-required' };
88
+ }
89
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
90
+ const unknownKey = (value, allowed) => Object.keys(value).find((k) => !allowed.includes(k)) ?? null;
91
+ const nonEmpty = (value) => typeof value === 'string' && value.length > 0;
92
+ const validDate = (value) => typeof value === 'string' && !Number.isNaN(Date.parse(value));
93
+ const validInt = (value, min) => typeof value === 'number' && Number.isInteger(value) && value >= min;
94
+ const utf8Bytes = (text) => new TextEncoder().encode(text);
95
+ const fail = (code, detail) => ({ ok: false, code, detail });
96
+ const SEAT_ONBOARDING_TOP_KEYS = [
97
+ 'schemaVersion', 'cli', 'seatRef', 'accountBinding', 'accountBindingBasis',
98
+ 'lineage', 'reboundFrom', 'reboundAt', 'nativeCliVersion', 'os',
99
+ 'capturedAt', 'consent', 'cellSynthesis', 'files',
100
+ ];
101
+ const SEAT_ONBOARDING_FILE_KEYS = ['destKey', 'format', 'contentBase64', 'sha256', 'byteLength'];
102
+ const LINEAGE_KEYS = ['epoch', 'generation', 'operationId'];
103
+ const CONSENT_KEYS = ['source', 'ownerScope'];
104
+ function validateLineageTuple(value) {
105
+ if (!isRecord(value))
106
+ return false;
107
+ if (unknownKey(value, LINEAGE_KEYS))
108
+ return false;
109
+ return validInt(value['epoch'], 1) && validInt(value['generation'], 0) && nonEmpty(value['operationId']);
110
+ }
111
+ class PbtxtParser {
112
+ text;
113
+ pos = 0;
114
+ constructor(text) {
115
+ this.text = text;
116
+ }
117
+ parseFields(untilBrace) {
118
+ const fields = [];
119
+ for (;;) {
120
+ this.skipTrivia();
121
+ if (this.pos >= this.text.length) {
122
+ if (untilBrace)
123
+ throw new SeatOnboardingError('onboarding-shape-unverified', 'unterminated pbtxt block');
124
+ return fields;
125
+ }
126
+ if (this.text[this.pos] === '}') {
127
+ if (!untilBrace)
128
+ throw new SeatOnboardingError('onboarding-shape-unverified', 'unexpected }');
129
+ this.pos += 1;
130
+ return fields;
131
+ }
132
+ fields.push(this.parseField());
133
+ }
134
+ }
135
+ parseField() {
136
+ const name = this.parseIdent();
137
+ this.skipTrivia();
138
+ if (this.pos >= this.text.length)
139
+ throw new SeatOnboardingError('onboarding-shape-unverified', 'truncated pbtxt field');
140
+ const ch = this.text[this.pos];
141
+ if (ch === '{') {
142
+ this.pos += 1;
143
+ return { name, children: this.parseFields(true) };
144
+ }
145
+ if (ch === ':') {
146
+ this.pos += 1;
147
+ return { name, scalar: this.parseScalar() };
148
+ }
149
+ throw new SeatOnboardingError('onboarding-shape-unverified', `unexpected '${ch}' after field ${name}`);
150
+ }
151
+ parseScalar() {
152
+ this.skipTrivia();
153
+ if (this.pos >= this.text.length)
154
+ throw new SeatOnboardingError('onboarding-shape-unverified', 'truncated pbtxt scalar');
155
+ const ch = this.text[this.pos];
156
+ if (ch === '"')
157
+ return { kind: 'string', value: this.parseString() };
158
+ if (ch === '-' || (ch >= '0' && ch <= '9'))
159
+ return { kind: 'number', value: this.parseNumber() };
160
+ return { kind: 'enum', token: this.parseIdent() };
161
+ }
162
+ parseIdent() {
163
+ this.skipTrivia();
164
+ const start = this.pos;
165
+ while (this.pos < this.text.length && /[A-Za-z0-9_.]/.test(this.text[this.pos]))
166
+ this.pos += 1;
167
+ if (this.pos === start)
168
+ throw new SeatOnboardingError('onboarding-shape-unverified', 'expected identifier');
169
+ return this.text.slice(start, this.pos);
170
+ }
171
+ parseString() {
172
+ this.pos += 1;
173
+ let out = '';
174
+ while (this.pos < this.text.length && this.text[this.pos] !== '"') {
175
+ out += this.text[this.pos];
176
+ this.pos += 1;
177
+ }
178
+ if (this.pos >= this.text.length)
179
+ throw new SeatOnboardingError('onboarding-shape-unverified', 'unterminated string');
180
+ this.pos += 1;
181
+ return out;
182
+ }
183
+ parseNumber() {
184
+ const start = this.pos;
185
+ while (this.pos < this.text.length && /[-+0-9.eE]/.test(this.text[this.pos]))
186
+ this.pos += 1;
187
+ return this.text.slice(start, this.pos);
188
+ }
189
+ skipTrivia() {
190
+ for (;;) {
191
+ while (this.pos < this.text.length && /\s/.test(this.text[this.pos]))
192
+ this.pos += 1;
193
+ if (this.text[this.pos] === '#') {
194
+ while (this.pos < this.text.length && this.text[this.pos] !== '\n')
195
+ this.pos += 1;
196
+ continue;
197
+ }
198
+ return;
199
+ }
200
+ }
201
+ }
202
+ /** Parse the pbtxt subset; throws SeatOnboardingError on shape drift. */
203
+ export function parseAgyJetskiPbtxt(text) {
204
+ return new PbtxtParser(text).parseFields(false);
205
+ }
206
+ /** Canonical serialization: fixed key order, reviewed enum order, one trailing newline. */
207
+ export function canonicalAgyJetskiBytes(nativeCliVersion, steps) {
208
+ const required = AGY_REQUIRED_COMPLETED_STEPS[nativeCliVersion];
209
+ if (!required)
210
+ throw new SeatOnboardingError('onboarding-shape-unverified', `no required step set for agy ${nativeCliVersion}`);
211
+ const ordered = required.filter((step) => steps.includes(step));
212
+ return `post_onboarding {\n${ordered.map((s) => ` completed_steps: ${s}\n`).join('')}}\n`;
213
+ }
214
+ /** True when any scalar anywhere in the tree is a quoted string or a number. */
215
+ function hasIdentifierScalar(fields) {
216
+ for (const field of fields) {
217
+ if (field.scalar && field.scalar.kind !== 'enum')
218
+ return true;
219
+ if (field.children && hasIdentifierScalar(field.children))
220
+ return true;
221
+ }
222
+ return false;
223
+ }
224
+ /** Extract the completed_steps set of the single post_onboarding block. */
225
+ function extractAgySteps(fields) {
226
+ const blocks = fields.filter((f) => f.name === 'post_onboarding');
227
+ if (fields.length !== 1 || blocks.length !== 1 || !blocks[0].children) {
228
+ return 'onboarding-shape-unverified';
229
+ }
230
+ const children = blocks[0].children;
231
+ if (children.length === 0)
232
+ return 'onboarding-incomplete';
233
+ const steps = [];
234
+ for (const child of children) {
235
+ if (child.name !== 'completed_steps' || !child.scalar || child.scalar.kind !== 'enum') {
236
+ return 'onboarding-shape-unverified';
237
+ }
238
+ steps.push(child.scalar.token);
239
+ }
240
+ return { steps };
241
+ }
242
+ /**
243
+ * Validate transported agy bytes: canonical form only. The step set must be
244
+ * exactly the full reviewed set for the pinned version; any extra key,
245
+ * value kind or step refuses.
246
+ */
247
+ export function validateAgyJetskiContent(text, nativeCliVersion) {
248
+ let fields;
249
+ try {
250
+ fields = parseAgyJetskiPbtxt(text);
251
+ }
252
+ catch (error) {
253
+ if (error instanceof SeatOnboardingError)
254
+ return error.code;
255
+ return 'onboarding-shape-unverified';
256
+ }
257
+ if (hasIdentifierScalar(fields))
258
+ return 'onboarding-identifier-present';
259
+ const extracted = extractAgySteps(fields);
260
+ if (typeof extracted === 'string')
261
+ return extracted;
262
+ const required = AGY_REQUIRED_COMPLETED_STEPS[nativeCliVersion];
263
+ if (!required)
264
+ return 'onboarding-shape-unverified';
265
+ const set = new Set(extracted.steps);
266
+ if (set.size !== extracted.steps.length)
267
+ return 'onboarding-shape-unverified';
268
+ for (const step of extracted.steps) {
269
+ if (!required.includes(step))
270
+ return 'onboarding-shape-unverified';
271
+ }
272
+ for (const step of required) {
273
+ if (!set.has(step))
274
+ return 'onboarding-incomplete';
275
+ }
276
+ if (text !== canonicalAgyJetskiBytes(nativeCliVersion, extracted.steps)) {
277
+ return 'onboarding-shape-unverified';
278
+ }
279
+ return null;
280
+ }
281
+ // ── per-format content validation ────────────────────────────────────────
282
+ // Structural, per format. Nothing is accepted on the basis of a file name
283
+ // or a digest alone.
284
+ export const CLAUDE_VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
285
+ export const MCP_SERVER_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
286
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/;
287
+ function decodeBase64Strict(value) {
288
+ if (typeof value !== 'string' || value.length === 0)
289
+ return null;
290
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0)
291
+ return null;
292
+ try {
293
+ return new Uint8Array(Buffer.from(value, 'base64'));
294
+ }
295
+ catch {
296
+ return null;
297
+ }
298
+ }
299
+ function parseJsonObject(bytes) {
300
+ let parsed;
301
+ try {
302
+ parsed = JSON.parse(new TextDecoder().decode(bytes));
303
+ }
304
+ catch {
305
+ return null;
306
+ }
307
+ return isRecord(parsed) ? parsed : null;
308
+ }
309
+ /** Validate one transported file's decoded content against its format. */
310
+ export function validateOnboardingFileContent(format, bytes, nativeCliVersion) {
311
+ if (format === 'agy-jetski-pbtxt-v1') {
312
+ let text;
313
+ try {
314
+ text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
315
+ }
316
+ catch {
317
+ return 'onboarding-shape-unverified';
318
+ }
319
+ return validateAgyJetskiContent(text, nativeCliVersion);
320
+ }
321
+ const obj = parseJsonObject(bytes);
322
+ if (!obj)
323
+ return 'onboarding-shape-unverified';
324
+ if (format === 'claude-json-projection-v1') {
325
+ if (unknownKey(obj, ['hasCompletedOnboarding', 'lastOnboardingVersion']))
326
+ return 'onboarding-shape-unverified';
327
+ if (obj['hasCompletedOnboarding'] !== true)
328
+ return 'onboarding-shape-unverified';
329
+ if (typeof obj['lastOnboardingVersion'] !== 'string' || !CLAUDE_VERSION_PATTERN.test(obj['lastOnboardingVersion'])) {
330
+ return 'onboarding-shape-unverified';
331
+ }
332
+ return null;
333
+ }
334
+ if (format === 'muse-trust-json-v1') {
335
+ if (unknownKey(obj, ['schema_version']))
336
+ return 'onboarding-shape-unverified';
337
+ const schemaVersion = obj['schema_version'];
338
+ if (!validInt(schemaVersion, 1) || !MUSE_TRUST_KNOWN_SCHEMA_VERSIONS.includes(schemaVersion)) {
339
+ return 'onboarding-shape-unverified';
340
+ }
341
+ return null;
342
+ }
343
+ if (format === 'codex-config-projection-v1') {
344
+ if (unknownKey(obj, ['approvalPolicy', 'mcpServerNames']))
345
+ return 'onboarding-shape-unverified';
346
+ if (typeof obj['approvalPolicy'] !== 'string' || !CODEX_APPROVAL_POLICY_VALUES.includes(obj['approvalPolicy'])) {
347
+ return 'onboarding-shape-unverified';
348
+ }
349
+ const names = obj['mcpServerNames'];
350
+ if (!Array.isArray(names) || names.length === 0)
351
+ return 'onboarding-shape-unverified';
352
+ const seen = new Set();
353
+ for (const name of names) {
354
+ if (typeof name !== 'string' || !MCP_SERVER_NAME_PATTERN.test(name) || seen.has(name)) {
355
+ return 'onboarding-shape-unverified';
356
+ }
357
+ seen.add(name);
358
+ }
359
+ return null;
360
+ }
361
+ return 'onboarding-shape-unverified';
362
+ }
363
+ /** Structural validation of one onboarding block; binding checks live in validateSeatEnvelope. */
364
+ export function validateSeatOnboarding(block) {
365
+ let serialized = '';
366
+ try {
367
+ serialized = JSON.stringify(block) ?? '';
368
+ }
369
+ catch {
370
+ return fail('onboarding-schema-mismatch', 'block is not JSON-serializable');
371
+ }
372
+ if (utf8Bytes(serialized).length > SEAT_ONBOARDING_MAX_BYTES) {
373
+ return fail('onboarding-size-exceeded', 'onboarding block exceeds 4 KiB');
374
+ }
375
+ if (!isRecord(block))
376
+ return fail('onboarding-schema-mismatch', 'block must be an object');
377
+ const badTop = unknownKey(block, SEAT_ONBOARDING_TOP_KEYS);
378
+ if (badTop)
379
+ return fail('onboarding-unknown-field', `unknown onboarding field: ${badTop}`);
380
+ if (block['schemaVersion'] !== SEAT_ONBOARDING_SCHEMA_VERSION) {
381
+ return fail('onboarding-schema-mismatch', 'schemaVersion must be 1');
382
+ }
383
+ const cli = block['cli'];
384
+ if (!CLIS.includes(cli))
385
+ return fail('onboarding-schema-mismatch', 'cli must be one of the five seat CLIs');
386
+ const destKey = CLI_DEST[cli];
387
+ if (!destKey)
388
+ return fail('onboarding-shape-unverified', 'no v1 onboarding allowlist entry for this CLI');
389
+ if (!nonEmpty(block['seatRef']))
390
+ return fail('onboarding-schema-mismatch', 'seatRef must be non-empty');
391
+ if (!nonEmpty(block['accountBinding']))
392
+ return fail('onboarding-schema-mismatch', 'accountBinding must be non-empty');
393
+ if (block['accountBindingBasis'] !== 'custodian-stable-ref') {
394
+ return fail('onboarding-schema-mismatch', 'accountBindingBasis must be custodian-stable-ref');
395
+ }
396
+ if (!validateLineageTuple(block['lineage']))
397
+ return fail('onboarding-schema-mismatch', 'lineage must be a valid tuple');
398
+ const hasReboundFrom = block['reboundFrom'] !== undefined;
399
+ const hasReboundAt = block['reboundAt'] !== undefined;
400
+ if (hasReboundFrom !== hasReboundAt)
401
+ return fail('onboarding-schema-mismatch', 'reboundFrom and reboundAt come together');
402
+ if (hasReboundFrom && !validateLineageTuple(block['reboundFrom'])) {
403
+ return fail('onboarding-schema-mismatch', 'reboundFrom must be a valid tuple');
404
+ }
405
+ if (hasReboundAt && !validDate(block['reboundAt']))
406
+ return fail('onboarding-schema-mismatch', 'reboundAt must be a valid date');
407
+ if (!nonEmpty(block['nativeCliVersion']))
408
+ return fail('onboarding-schema-mismatch', 'nativeCliVersion must be non-empty');
409
+ if (block['os'] !== 'linux' && block['os'] !== 'macos' && block['os'] !== 'windows') {
410
+ return fail('onboarding-schema-mismatch', 'os must be linux, macos or windows');
411
+ }
412
+ if (!validDate(block['capturedAt']))
413
+ return fail('onboarding-schema-mismatch', 'capturedAt must be a valid custodian-observed date');
414
+ const consent = block['consent'];
415
+ if (!isRecord(consent))
416
+ return fail('onboarding-schema-mismatch', 'consent must be an object');
417
+ const badConsent = unknownKey(consent, CONSENT_KEYS);
418
+ if (badConsent)
419
+ return fail('onboarding-unknown-field', `unknown consent field: ${badConsent}`);
420
+ if (consent['source'] !== 'owner-interactive-cli')
421
+ return fail('onboarding-schema-mismatch', 'consent.source must be owner-interactive-cli');
422
+ if (!nonEmpty(consent['ownerScope']))
423
+ return fail('onboarding-schema-mismatch', 'consent.ownerScope must be non-empty');
424
+ const synthesis = block['cellSynthesis'];
425
+ if (!Array.isArray(synthesis))
426
+ return fail('onboarding-schema-mismatch', 'cellSynthesis must be an array');
427
+ const seenSynth = new Set();
428
+ for (const entry of synthesis) {
429
+ if (typeof entry !== 'string' || !(entry in SYNTHESIS_CLIS) || seenSynth.has(entry)) {
430
+ return fail('onboarding-shape-unverified', 'cellSynthesis entries must be unique known synthesis keys');
431
+ }
432
+ if (SYNTHESIS_CLIS[entry] !== cli) {
433
+ return fail('onboarding-shape-unverified', 'cellSynthesis entry does not belong to this CLI');
434
+ }
435
+ seenSynth.add(entry);
436
+ }
437
+ if (cli === 'agy' && synthesis.length > 0) {
438
+ return fail('onboarding-shape-unverified', 'agy carries no cellSynthesis entry');
439
+ }
440
+ const files = block['files'];
441
+ if (!Array.isArray(files) || files.length !== 1) {
442
+ return fail('onboarding-shape-unverified', 'v1 carries exactly one onboarding file per CLI');
443
+ }
444
+ const file = files[0];
445
+ if (!isRecord(file))
446
+ return fail('onboarding-schema-mismatch', 'file must be an object');
447
+ const badFile = unknownKey(file, SEAT_ONBOARDING_FILE_KEYS);
448
+ if (badFile)
449
+ return fail('onboarding-unknown-field', `unknown file field: ${badFile}`);
450
+ if (file['destKey'] !== destKey)
451
+ return fail('onboarding-shape-unverified', 'destKey does not match the CLI allowlist');
452
+ if (file['format'] !== DEST_FORMAT[destKey])
453
+ return fail('onboarding-shape-unverified', 'format does not match the destination');
454
+ const bytes = decodeBase64Strict(file['contentBase64']);
455
+ if (!bytes)
456
+ return fail('onboarding-schema-mismatch', 'contentBase64 must be strict base64');
457
+ if (!validInt(file['byteLength'], 1) || file['byteLength'] !== bytes.length) {
458
+ return fail('onboarding-shape-unverified', 'byteLength does not match the decoded content');
459
+ }
460
+ if (bytes.length > SEAT_ONBOARDING_FILE_CAPS[destKey]) {
461
+ return fail('onboarding-size-exceeded', 'onboarding file exceeds its decoded-byte cap');
462
+ }
463
+ if (typeof file['sha256'] !== 'string' || !SHA256_PATTERN.test(file['sha256'])) {
464
+ return fail('onboarding-shape-unverified', 'sha256 must be a hex digest');
465
+ }
466
+ const digest = createHash('sha256').update(bytes).digest('hex');
467
+ if (digest !== file['sha256'])
468
+ return fail('onboarding-shape-unverified', 'sha256 does not match the decoded content');
469
+ const contentCode = validateOnboardingFileContent(DEST_FORMAT[destKey], bytes, block['nativeCliVersion']);
470
+ if (contentCode)
471
+ return fail(contentCode, 'transported content is not canonical for the pinned version');
472
+ return { ok: true, block: block };
473
+ }
474
+ // ── envelope validation ──────────────────────────────────────────────────
475
+ const ENVELOPE_KEYS = ['bundle', 'files', 'agy', 'onboarding'];
476
+ /**
477
+ * Validate the whole serialized seat envelope as one unit: bundle, agy
478
+ * tuple translation where applicable, auth-file allowlist, onboarding
479
+ * validation and binding. An absent block is a valid envelope (D1); what
480
+ * absence denies is launch, through seatLaunchGate — never envelope
481
+ * validity. `onboarding-required` is never returned here.
482
+ */
483
+ export function validateSeatEnvelope(envelope, ctx) {
484
+ let serialized = '';
485
+ try {
486
+ serialized = JSON.stringify(envelope) ?? '';
487
+ }
488
+ catch {
489
+ return { ok: false, code: 'schema-mismatch', detail: 'envelope is not JSON-serializable' };
490
+ }
491
+ if (utf8Bytes(serialized).length > SEAT_BUNDLE_MAX_BYTES) {
492
+ return { ok: false, code: 'size-exceeded', detail: 'serialized envelope exceeds 48 KiB' };
493
+ }
494
+ if (!isRecord(envelope))
495
+ return { ok: false, code: 'schema-mismatch', detail: 'envelope must be an object' };
496
+ const badKey = unknownKey(envelope, ENVELOPE_KEYS);
497
+ if (badKey)
498
+ return { ok: false, code: 'unknown-field', detail: `unknown envelope field: ${badKey}` };
499
+ const bundleResult = validateSeatBundle(envelope['bundle'], {
500
+ ...ctx,
501
+ trustedAuthority: ctx?.trustedAuthority ?? 'consumer',
502
+ });
503
+ if (!bundleResult.ok)
504
+ return { ok: false, code: bundleResult.code, detail: bundleResult.detail };
505
+ const bundle = bundleResult.bundle;
506
+ const cli = bundle.identity.cli;
507
+ const filesRaw = envelope['files'] ?? {};
508
+ if (!isRecord(filesRaw))
509
+ return { ok: false, code: 'schema-mismatch', detail: 'files must be an object' };
510
+ const allowlist = SEAT_ENVELOPE_AUTH_FILES[cli];
511
+ for (const [name, value] of Object.entries(filesRaw)) {
512
+ if (!allowlist.includes(name)) {
513
+ return { ok: false, code: 'unknown-field', detail: `auth file not allowlisted for ${cli}: ${name}` };
514
+ }
515
+ if (typeof value !== 'string' || decodeBase64Strict(value) === null) {
516
+ return { ok: false, code: 'schema-mismatch', detail: `auth file ${name} must be base64` };
517
+ }
518
+ }
519
+ let agy = null;
520
+ if (cli === 'agy') {
521
+ if (!isRecord(envelope['agy'])) {
522
+ return { ok: false, code: 'schema-mismatch', detail: 'agy envelope member is required for agy' };
523
+ }
524
+ agy = envelope['agy'];
525
+ try {
526
+ translateAgySeatToApplication({
527
+ ownerScope: bundle.identity.ownerScope,
528
+ seatRef: bundle.identity.seatRef,
529
+ accountBinding: bundle.provenance.accountBinding,
530
+ ...agy,
531
+ });
532
+ }
533
+ catch (error) {
534
+ if (error instanceof AgyMappingError) {
535
+ return { ok: false, code: 'client-mismatch', detail: `agy tuple refused: ${error.code}` };
536
+ }
537
+ throw error;
538
+ }
539
+ }
540
+ else if (envelope['agy'] !== undefined) {
541
+ return { ok: false, code: 'unknown-field', detail: 'agy member is only valid for agy seats' };
542
+ }
543
+ let onboarding = null;
544
+ let onboardingStatus = 'absent';
545
+ if (envelope['onboarding'] !== undefined) {
546
+ // A present block for a CLI with no allowlist entry (vibe in v1) is an
547
+ // envelope defect, never a silent drop.
548
+ if (!CLI_DEST[cli]) {
549
+ return { ok: false, code: 'onboarding-shape-unverified', detail: 'no v1 onboarding allowlist entry for this CLI' };
550
+ }
551
+ const blockResult = validateSeatOnboarding(envelope['onboarding']);
552
+ if (!blockResult.ok)
553
+ return { ok: false, code: blockResult.code, detail: blockResult.detail };
554
+ const block = blockResult.block;
555
+ if (block.cli !== cli)
556
+ return { ok: false, code: 'onboarding-lineage-mismatch', detail: 'block cli does not match the bundle' };
557
+ if (block.seatRef !== bundle.identity.seatRef) {
558
+ return { ok: false, code: 'onboarding-lineage-mismatch', detail: 'block seatRef does not match the bundle' };
559
+ }
560
+ if (block.accountBinding !== bundle.provenance.accountBinding) {
561
+ return { ok: false, code: 'onboarding-lineage-mismatch', detail: 'block accountBinding does not match the bundle' };
562
+ }
563
+ if (block.consent.ownerScope !== bundle.identity.ownerScope) {
564
+ return { ok: false, code: 'onboarding-lineage-mismatch', detail: 'block consent ownerScope does not match the bundle' };
565
+ }
566
+ const lifecycle = bundle.lifecycle;
567
+ if (block.lineage.epoch !== lifecycle.epoch ||
568
+ block.lineage.generation !== lifecycle.generation ||
569
+ block.lineage.operationId !== lifecycle.operationId) {
570
+ return { ok: false, code: 'onboarding-lineage-mismatch', detail: 'block lineage does not match the bundle lifecycle' };
571
+ }
572
+ if (bundle.identity.nativeCliVersion !== block.nativeCliVersion) {
573
+ return { ok: false, code: 'onboarding-version-mismatch', detail: 'bundle nativeCliVersion must equal the block version' };
574
+ }
575
+ onboarding = block;
576
+ onboardingStatus = 'present';
577
+ }
578
+ return {
579
+ ok: true,
580
+ bundle,
581
+ files: filesRaw,
582
+ agy,
583
+ onboarding,
584
+ onboardingStatus,
585
+ launchGate: seatLaunchGate(cli, onboardingStatus),
586
+ };
587
+ }
588
+ /**
589
+ * D11 rebind: move only the lineage pointer on a same-epoch forward
590
+ * transition. Never recaptures and never edits content; an epoch change or
591
+ * a CLI version change requires a fresh interactive capture.
592
+ */
593
+ export function rebindSeatOnboarding(block, nextBundle, options) {
594
+ if (!validDate(options.reboundAt)) {
595
+ return fail('onboarding-schema-mismatch', 'reboundAt must be a valid custodian-observed date');
596
+ }
597
+ const incoming = {
598
+ epoch: nextBundle.lifecycle.epoch,
599
+ generation: nextBundle.lifecycle.generation,
600
+ operationId: nextBundle.lifecycle.operationId,
601
+ };
602
+ if (classifyTransition(block.lineage, incoming) !== 'forward') {
603
+ return fail('onboarding-lineage-mismatch', 'rebind requires a same-epoch forward transition');
604
+ }
605
+ if (nextBundle.identity.seatRef !== block.seatRef) {
606
+ return fail('onboarding-lineage-mismatch', 'rebind requires the same seatRef');
607
+ }
608
+ if (nextBundle.provenance.accountBinding !== block.accountBinding) {
609
+ return fail('onboarding-lineage-mismatch', 'rebind requires the same asserted account');
610
+ }
611
+ if (nextBundle.identity.nativeCliVersion !== block.nativeCliVersion) {
612
+ return fail('onboarding-version-mismatch', 'rebind requires an unchanged nativeCliVersion');
613
+ }
614
+ if (options.files !== undefined) {
615
+ if (options.files.length !== block.files.length) {
616
+ return fail('onboarding-shape-unverified', 'rebind requires identical file digests');
617
+ }
618
+ for (let i = 0; i < options.files.length; i += 1) {
619
+ const next = options.files[i];
620
+ const prev = block.files[i];
621
+ if (next.destKey !== prev.destKey ||
622
+ next.format !== prev.format ||
623
+ next.sha256 !== prev.sha256 ||
624
+ next.byteLength !== prev.byteLength ||
625
+ next.contentBase64 !== prev.contentBase64) {
626
+ return fail('onboarding-shape-unverified', 'rebind requires identical file digests');
627
+ }
628
+ }
629
+ }
630
+ return {
631
+ ok: true,
632
+ block: {
633
+ ...block,
634
+ lineage: incoming,
635
+ reboundFrom: { ...block.lineage },
636
+ reboundAt: options.reboundAt,
637
+ },
638
+ };
639
+ }
640
+ // ── capture-side agy canonicalizer ───────────────────────────────────────
641
+ /** Capture extraction: the single post_onboarding block among other top-level keys. */
642
+ function extractAgyStepsForCapture(fields) {
643
+ const blocks = fields.filter((f) => f.name === 'post_onboarding');
644
+ if (blocks.length !== 1 || !blocks[0].children)
645
+ return 'onboarding-shape-unverified';
646
+ const children = blocks[0].children;
647
+ if (children.length === 0)
648
+ return 'onboarding-incomplete';
649
+ const steps = [];
650
+ for (const child of children) {
651
+ if (child.scalar && child.scalar.kind !== 'enum')
652
+ return 'onboarding-identifier-present';
653
+ if (child.name !== 'completed_steps' || !child.scalar || child.scalar.kind !== 'enum') {
654
+ return 'onboarding-shape-unverified';
655
+ }
656
+ steps.push(child.scalar.token);
657
+ }
658
+ return { steps };
659
+ }
660
+ /**
661
+ * Capture-side canonicalizer: parse the source file, keep only the
662
+ * allowlisted step set, check it against the pinned version's required
663
+ * set, and re-serialize canonically. No code path copies source bytes
664
+ * through; `installation_uuid` and `migrations` are discarded here.
665
+ */
666
+ export function canonicaliseAgyJetski(sourceText, nativeCliVersion) {
667
+ let fields;
668
+ try {
669
+ fields = parseAgyJetskiPbtxt(sourceText);
670
+ }
671
+ catch (error) {
672
+ if (error instanceof SeatOnboardingError)
673
+ throw error;
674
+ throw new SeatOnboardingError('onboarding-shape-unverified', 'source pbtxt is not parseable');
675
+ }
676
+ const extracted = extractAgyStepsForCapture(fields);
677
+ if (typeof extracted === 'string')
678
+ throw new SeatOnboardingError(extracted, 'source step set refused');
679
+ const required = AGY_REQUIRED_COMPLETED_STEPS[nativeCliVersion];
680
+ if (!required) {
681
+ throw new SeatOnboardingError('onboarding-shape-unverified', `no required step set for agy ${nativeCliVersion}`);
682
+ }
683
+ const set = new Set(extracted.steps);
684
+ if (set.size !== extracted.steps.length) {
685
+ throw new SeatOnboardingError('onboarding-shape-unverified', 'duplicate step entries');
686
+ }
687
+ for (const step of extracted.steps) {
688
+ if (!required.includes(step))
689
+ throw new SeatOnboardingError('onboarding-shape-unverified', 'unknown step enum');
690
+ }
691
+ for (const step of required) {
692
+ if (!set.has(step)) {
693
+ throw new SeatOnboardingError('onboarding-incomplete', 'source step set is a strict subset of the required set');
694
+ }
695
+ }
696
+ return canonicalAgyJetskiBytes(nativeCliVersion, extracted.steps);
697
+ }
698
+ //# sourceMappingURL=seat-onboarding.js.map