@indigoai-us/hq-cli 5.115.1 → 5.115.2

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 (32) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/assets/scaffold/core/scripts/qmd-reindex-after-sync.sh +10 -87
  3. package/dist/commands/bot.js +1 -1
  4. package/dist/commands/index-cmd.d.ts +8 -1
  5. package/dist/commands/index-cmd.js +15 -5
  6. package/dist/commands/onboard-identity-guard.d.ts +1 -1
  7. package/dist/commands/onboard.js +1 -1
  8. package/dist/lib/bot/prompt.d.ts +8 -1
  9. package/dist/lib/bot/prompt.js +10 -2
  10. package/dist/lib/bot/run.js +1 -1
  11. package/dist/lib/core-utils/qmd-reindex-after-sync.d.ts +4 -1
  12. package/dist/lib/core-utils/qmd-reindex-after-sync.js +11 -2
  13. package/dist/lib/onboarding/checkpoint.d.ts +13 -0
  14. package/dist/lib/onboarding/checkpoint.js +44 -0
  15. package/dist/lib/onboarding/cli/onboard.d.ts +53 -0
  16. package/dist/lib/onboarding/cli/onboard.js +135 -0
  17. package/dist/lib/onboarding/cli/prompts.d.ts +28 -0
  18. package/dist/lib/onboarding/cli/prompts.js +83 -0
  19. package/dist/lib/onboarding/errors.d.ts +33 -0
  20. package/dist/lib/onboarding/errors.js +59 -0
  21. package/dist/lib/onboarding/index.d.ts +17 -0
  22. package/dist/lib/onboarding/index.js +16 -0
  23. package/dist/lib/onboarding/orchestrator.d.ts +47 -0
  24. package/dist/lib/onboarding/orchestrator.js +569 -0
  25. package/dist/lib/onboarding/types.d.ts +79 -0
  26. package/dist/lib/onboarding/types.js +8 -0
  27. package/dist/lib/search-index/background.js +2 -200
  28. package/dist/lib/search-index/embed-lock.d.ts +38 -0
  29. package/dist/lib/search-index/embed-lock.js +286 -0
  30. package/dist/lib/search-index/index.d.ts +1 -0
  31. package/dist/lib/search-index/index.js +4 -1
  32. package/package.json +1 -2
@@ -0,0 +1,569 @@
1
+ /**
2
+ * Onboarding orchestrator (VLT-9 US-001; vendored from @indigoai-us/hq-onboarding).
3
+ *
4
+ * Composes VLT-1 (entities), VLT-2 (bucket provisioning), VLT-3 (STS),
5
+ * VLT-5 (sync), VLT-6 (membership), and VLT-7 (invite/accept) into two
6
+ * end-to-end flows:
7
+ *
8
+ * createCompanyFlow — founder creates a new company + vault
9
+ * joinCompanyFlow — invitee accepts an invite and syncs
10
+ *
11
+ * Each step is idempotent via checkpoint/resume. Progress events are
12
+ * emitted via callback for the installer UI.
13
+ */
14
+ import { writeFile, mkdir } from "node:fs/promises";
15
+ import { join, dirname } from "node:path";
16
+ import { VaultClient, VaultConflictError, VaultNotFoundError, resolveEntityContext, sync, parseToken, } from "@indigoai-us/hq-cloud";
17
+ import { PersonCreationError, CompanyCreationError, ProvisioningError, MembershipBootstrapError, StsVerifyError, InviteAcceptError, FirstSyncError, } from "./errors.js";
18
+ import { readCheckpoint, writeCheckpoint, isStepComplete, deleteCheckpoint, } from "./checkpoint.js";
19
+ // ---------------------------------------------------------------------------
20
+ // Public API
21
+ // ---------------------------------------------------------------------------
22
+ /**
23
+ * Create-company flow for founders.
24
+ *
25
+ * Steps:
26
+ * 1. Create person entity
27
+ * 2. Create company entity (the vault-service makes the caller its owner)
28
+ * 3. Provision bucket + KMS via vault-service
29
+ * 4. Bootstrap owner membership (library-direct, bypasses handler auth)
30
+ * 5. Verify STS vend works end-to-end
31
+ * 6. Write .hq/config.json
32
+ */
33
+ export async function createCompanyFlow(input, config, onProgress) {
34
+ const client = new VaultClient(config.vaultConfig);
35
+ const checkpoint = await readCheckpoint(config.hqRoot) ?? makeCheckpoint("create-company");
36
+ // Step 1: Create person
37
+ //
38
+ // Three-layer idempotency:
39
+ // a. Resume from checkpoint UID if present (handles mid-flight retries)
40
+ // b. Look up by slug on the server (handles fresh runs against an existing
41
+ // account — defends against the server not enforcing slug uniqueness)
42
+ // c. Create, with a VaultConflictError fallback for the create-time race
43
+ if (!isStepComplete(checkpoint, "create-person")) {
44
+ emit(onProgress, "create-person", "running");
45
+ try {
46
+ const personSlug = slugFromEmail(input.personEmail);
47
+ let person = (checkpoint.personUid
48
+ ? await safeGetEntity(client, checkpoint.personUid)
49
+ : null) ?? (await safeFindBySlug(client, "person", personSlug));
50
+ let detail;
51
+ if (person) {
52
+ detail = `Person already registered (${person.uid})`;
53
+ }
54
+ else {
55
+ try {
56
+ person = await client.entity.create({
57
+ type: "person",
58
+ slug: personSlug,
59
+ name: input.personName,
60
+ email: input.personEmail,
61
+ });
62
+ }
63
+ catch (err) {
64
+ if (err instanceof VaultConflictError) {
65
+ // Lost the create-time race — another caller created between our
66
+ // findBySlug and create. Re-fetch.
67
+ person = await client.entity.findBySlug("person", personSlug);
68
+ }
69
+ else {
70
+ throw err;
71
+ }
72
+ }
73
+ detail = `personUid: ${person.uid}`;
74
+ }
75
+ checkpoint.personUid = person.uid;
76
+ checkpoint.completedSteps.push("create-person");
77
+ await writeCheckpoint(config.hqRoot, checkpoint);
78
+ emit(onProgress, "create-person", "done", detail);
79
+ }
80
+ catch (err) {
81
+ checkpoint.failedStep = "create-person";
82
+ checkpoint.error = String(err);
83
+ await writeCheckpoint(config.hqRoot, checkpoint);
84
+ emit(onProgress, "create-person", "failed");
85
+ throw new PersonCreationError(`Failed to create person entity: ${err}`, err instanceof Error ? err : undefined);
86
+ }
87
+ }
88
+ else {
89
+ emit(onProgress, "create-person", "skipped", "Already complete");
90
+ }
91
+ // Step 2: Create company (same three-layer idempotency as step 1)
92
+ //
93
+ // The slug lookup is scoped to the CALLER's namespace (owned or member-of):
94
+ // under the vault-service's per-user-namespace model a global slug lookup
95
+ // could adopt a stranger's company, or 409 when several tenants share the
96
+ // slug. A company the caller already owns (say, from an earlier run that
97
+ // died before the bucket was provisioned) is picked up here and the rest of
98
+ // the flow completes against it.
99
+ //
100
+ // No `ownerUid` is sent with the create: the vault-service authorizes a
101
+ // human caller as their sign-in and makes that sign-in the company's owner.
102
+ // Sending the person entity's `prs_…` uid instead is refused with
103
+ // "ownerUid must match the authenticated caller" (the bug that broke every
104
+ // `hq onboard create-company` in 2026-09).
105
+ if (!isStepComplete(checkpoint, "create-company")) {
106
+ emit(onProgress, "create-company", "running");
107
+ try {
108
+ let company = (checkpoint.companyUid
109
+ ? await safeGetEntity(client, checkpoint.companyUid)
110
+ : null) ?? (await client.entity.findInMyNamespace("company", input.companySlug));
111
+ let detail;
112
+ if (company) {
113
+ detail = `Company already exists (${company.uid})`;
114
+ }
115
+ else {
116
+ try {
117
+ company = await client.entity.create({
118
+ type: "company",
119
+ slug: input.companySlug,
120
+ name: input.companyName,
121
+ });
122
+ }
123
+ catch (err) {
124
+ if (err instanceof VaultConflictError) {
125
+ // Lost the create-time race — re-fetch from our own namespace. A
126
+ // conflict that is NOT ours means another account holds the slug.
127
+ const mine = await client.entity.findInMyNamespace("company", input.companySlug);
128
+ if (!mine) {
129
+ throw new Error(`Company slug "${input.companySlug}" is already taken by another account. Choose another.`);
130
+ }
131
+ company = mine;
132
+ }
133
+ else {
134
+ throw err;
135
+ }
136
+ }
137
+ detail = `companyUid: ${company.uid}`;
138
+ }
139
+ checkpoint.companyUid = company.uid;
140
+ checkpoint.companySlug = company.slug;
141
+ checkpoint.completedSteps.push("create-company");
142
+ await writeCheckpoint(config.hqRoot, checkpoint);
143
+ emit(onProgress, "create-company", "done", detail);
144
+ }
145
+ catch (err) {
146
+ checkpoint.failedStep = "create-company";
147
+ checkpoint.error = String(err);
148
+ await writeCheckpoint(config.hqRoot, checkpoint);
149
+ emit(onProgress, "create-company", "failed");
150
+ throw new CompanyCreationError(`Failed to create company entity: ${err}`, err instanceof Error ? err : undefined);
151
+ }
152
+ }
153
+ else {
154
+ emit(onProgress, "create-company", "skipped", "Already complete");
155
+ }
156
+ // Step 3: Provision bucket + KMS
157
+ if (!isStepComplete(checkpoint, "provision-bucket")) {
158
+ emit(onProgress, "provision-bucket", "running");
159
+ try {
160
+ // Trigger provisioning via vault-service (server-side Lambda invocation)
161
+ const provisionResult = await client.provisionBucket(checkpoint.companyUid);
162
+ checkpoint.bucketName = provisionResult.bucketName;
163
+ checkpoint.completedSteps.push("provision-bucket");
164
+ await writeCheckpoint(config.hqRoot, checkpoint);
165
+ emit(onProgress, "provision-bucket", "done", `bucket: ${checkpoint.bucketName}`);
166
+ }
167
+ catch (err) {
168
+ // If bucket already provisioned, that's fine — fetch entity to get bucketName
169
+ const entity = await safeGetEntity(client, checkpoint.companyUid);
170
+ if (entity?.bucketName) {
171
+ checkpoint.bucketName = entity.bucketName;
172
+ checkpoint.completedSteps.push("provision-bucket");
173
+ await writeCheckpoint(config.hqRoot, checkpoint);
174
+ emit(onProgress, "provision-bucket", "skipped", "Already provisioned");
175
+ }
176
+ else {
177
+ checkpoint.failedStep = "provision-bucket";
178
+ checkpoint.error = String(err);
179
+ await writeCheckpoint(config.hqRoot, checkpoint);
180
+ emit(onProgress, "provision-bucket", "failed");
181
+ throw new ProvisioningError(`Bucket provisioning failed: ${err}`, err instanceof Error ? err : undefined);
182
+ }
183
+ }
184
+ }
185
+ else {
186
+ emit(onProgress, "provision-bucket", "skipped", "Already complete");
187
+ }
188
+ // Step 4: Bootstrap owner membership (library-direct, bypasses handler auth)
189
+ if (!isStepComplete(checkpoint, "bootstrap-membership")) {
190
+ emit(onProgress, "bootstrap-membership", "running");
191
+ try {
192
+ // Verify no existing memberships (optimistic concurrency guard)
193
+ const existing = await client.listMembersOfCompany(checkpoint.companyUid);
194
+ if (existing.length > 0) {
195
+ // Already has members — find our membership
196
+ const ours = existing.find(m => m.personUid === checkpoint.personUid);
197
+ if (ours) {
198
+ checkpoint.membershipKey = ours.membershipKey;
199
+ checkpoint.completedSteps.push("bootstrap-membership");
200
+ await writeCheckpoint(config.hqRoot, checkpoint);
201
+ emit(onProgress, "bootstrap-membership", "skipped", "Owner membership already exists");
202
+ }
203
+ else {
204
+ throw new MembershipBootstrapError("Company already has members but none match the founder — possible race condition");
205
+ }
206
+ }
207
+ else {
208
+ // Create invite + immediately accept under founder's identity
209
+ const invite = await client.createInvite({
210
+ companyUid: checkpoint.companyUid,
211
+ personUid: checkpoint.personUid,
212
+ role: "owner",
213
+ invitedBy: checkpoint.personUid,
214
+ });
215
+ const accept = await client.acceptInvite(invite.inviteToken, checkpoint.personUid);
216
+ checkpoint.membershipKey = accept.membership.membershipKey;
217
+ checkpoint.completedSteps.push("bootstrap-membership");
218
+ await writeCheckpoint(config.hqRoot, checkpoint);
219
+ emit(onProgress, "bootstrap-membership", "done", `role: owner`);
220
+ }
221
+ }
222
+ catch (err) {
223
+ if (err instanceof MembershipBootstrapError)
224
+ throw err;
225
+ checkpoint.failedStep = "bootstrap-membership";
226
+ checkpoint.error = String(err);
227
+ await writeCheckpoint(config.hqRoot, checkpoint);
228
+ emit(onProgress, "bootstrap-membership", "failed");
229
+ throw new MembershipBootstrapError(`Owner membership bootstrap failed: ${err}`, err instanceof Error ? err : undefined);
230
+ }
231
+ }
232
+ else {
233
+ emit(onProgress, "bootstrap-membership", "skipped", "Already complete");
234
+ }
235
+ // Step 5: Verify STS vend works end-to-end
236
+ if (!isStepComplete(checkpoint, "verify-sts")) {
237
+ emit(onProgress, "verify-sts", "running");
238
+ try {
239
+ const ctx = await resolveEntityContext(checkpoint.companyUid, config.vaultConfig);
240
+ if (!ctx.credentials?.accessKeyId) {
241
+ throw new Error("STS vend returned empty credentials");
242
+ }
243
+ checkpoint.completedSteps.push("verify-sts");
244
+ await writeCheckpoint(config.hqRoot, checkpoint);
245
+ emit(onProgress, "verify-sts", "done", `Credentials valid until ${ctx.expiresAt}`);
246
+ }
247
+ catch (err) {
248
+ checkpoint.failedStep = "verify-sts";
249
+ checkpoint.error = String(err);
250
+ await writeCheckpoint(config.hqRoot, checkpoint);
251
+ emit(onProgress, "verify-sts", "failed");
252
+ throw new StsVerifyError(`STS verification failed: ${err}`, err instanceof Error ? err : undefined);
253
+ }
254
+ }
255
+ else {
256
+ emit(onProgress, "verify-sts", "skipped", "Already complete");
257
+ }
258
+ // Step 6: Write .hq/config.json
259
+ emit(onProgress, "write-config", "running");
260
+ const configPath = await writeHqConfig(config.hqRoot, {
261
+ companyUid: checkpoint.companyUid,
262
+ companySlug: checkpoint.companySlug ?? input.companySlug,
263
+ personUid: checkpoint.personUid,
264
+ role: "owner",
265
+ bucketName: checkpoint.bucketName,
266
+ vaultApiUrl: config.vaultConfig.apiUrl,
267
+ configuredAt: new Date().toISOString(),
268
+ });
269
+ checkpoint.completedSteps.push("write-config");
270
+ await writeCheckpoint(config.hqRoot, checkpoint);
271
+ emit(onProgress, "write-config", "done", configPath);
272
+ // Clean up checkpoint on success
273
+ await deleteCheckpoint(config.hqRoot);
274
+ return {
275
+ personUid: checkpoint.personUid,
276
+ companyUid: checkpoint.companyUid,
277
+ companySlug: checkpoint.companySlug ?? input.companySlug,
278
+ role: "owner",
279
+ bucketName: checkpoint.bucketName,
280
+ configPath,
281
+ };
282
+ }
283
+ /**
284
+ * Join-company flow for invitees.
285
+ *
286
+ * Steps:
287
+ * 1. Parse invite token
288
+ * 2. Create person entity (if not already registered)
289
+ * 3. Accept invite
290
+ * 4. Verify STS vend
291
+ * 5. First sync to pull initial vault contents
292
+ * 6. Write .hq/config.json
293
+ */
294
+ export async function joinCompanyFlow(input, config, onProgress) {
295
+ const client = new VaultClient(config.vaultConfig);
296
+ const checkpoint = await readCheckpoint(config.hqRoot) ?? makeCheckpoint("join-company");
297
+ // Step 1: Parse token
298
+ emit(onProgress, "parse-token", "running");
299
+ const token = parseToken(input.inviteToken);
300
+ checkpoint.inviteToken = token;
301
+ await writeCheckpoint(config.hqRoot, checkpoint);
302
+ emit(onProgress, "parse-token", "done");
303
+ // Step 2: Create person (three-layer idempotency — same pattern as createCompanyFlow)
304
+ if (!isStepComplete(checkpoint, "create-person")) {
305
+ emit(onProgress, "create-person", "running");
306
+ try {
307
+ const personSlug = slugFromEmail(input.personEmail);
308
+ let person = (checkpoint.personUid
309
+ ? await safeGetEntity(client, checkpoint.personUid)
310
+ : null) ?? (await safeFindBySlug(client, "person", personSlug));
311
+ let detail;
312
+ if (person) {
313
+ detail = `Person already registered (${person.uid})`;
314
+ }
315
+ else {
316
+ try {
317
+ person = await client.entity.create({
318
+ type: "person",
319
+ slug: personSlug,
320
+ name: input.personName,
321
+ email: input.personEmail,
322
+ });
323
+ }
324
+ catch (err) {
325
+ if (err instanceof VaultConflictError) {
326
+ person = await client.entity.findBySlug("person", personSlug);
327
+ }
328
+ else {
329
+ throw err;
330
+ }
331
+ }
332
+ detail = `personUid: ${person.uid}`;
333
+ }
334
+ checkpoint.personUid = person.uid;
335
+ checkpoint.completedSteps.push("create-person");
336
+ await writeCheckpoint(config.hqRoot, checkpoint);
337
+ emit(onProgress, "create-person", "done", detail);
338
+ }
339
+ catch (err) {
340
+ checkpoint.failedStep = "create-person";
341
+ checkpoint.error = String(err);
342
+ await writeCheckpoint(config.hqRoot, checkpoint);
343
+ emit(onProgress, "create-person", "failed");
344
+ throw new PersonCreationError(`Failed to create person entity: ${err}`, err instanceof Error ? err : undefined);
345
+ }
346
+ }
347
+ else {
348
+ emit(onProgress, "create-person", "skipped", "Already complete");
349
+ }
350
+ // Step 3: Accept invite
351
+ if (!isStepComplete(checkpoint, "accept-invite")) {
352
+ emit(onProgress, "accept-invite", "running");
353
+ try {
354
+ const result = await client.acceptInvite(token, checkpoint.personUid);
355
+ checkpoint.companyUid = result.membership.companyUid;
356
+ checkpoint.membershipKey = result.membership.membershipKey;
357
+ // Resolve company slug
358
+ try {
359
+ const company = await client.entity.get(result.membership.companyUid);
360
+ checkpoint.companySlug = company.slug;
361
+ checkpoint.bucketName = company.bucketName;
362
+ }
363
+ catch {
364
+ // Non-critical — we have the UID
365
+ }
366
+ checkpoint.completedSteps.push("accept-invite");
367
+ await writeCheckpoint(config.hqRoot, checkpoint);
368
+ emit(onProgress, "accept-invite", "done", `role: ${result.membership.role}`);
369
+ }
370
+ catch (err) {
371
+ if (err instanceof VaultConflictError) {
372
+ // Already accepted — that's fine for resume
373
+ emit(onProgress, "accept-invite", "skipped", "Already accepted");
374
+ checkpoint.completedSteps.push("accept-invite");
375
+ await writeCheckpoint(config.hqRoot, checkpoint);
376
+ }
377
+ else {
378
+ checkpoint.failedStep = "accept-invite";
379
+ checkpoint.error = String(err);
380
+ await writeCheckpoint(config.hqRoot, checkpoint);
381
+ emit(onProgress, "accept-invite", "failed");
382
+ throw new InviteAcceptError(`Failed to accept invite: ${err}`, err instanceof Error ? err : undefined);
383
+ }
384
+ }
385
+ }
386
+ else {
387
+ emit(onProgress, "accept-invite", "skipped", "Already complete");
388
+ }
389
+ // Step 4: Verify STS vend
390
+ if (!isStepComplete(checkpoint, "verify-sts")) {
391
+ emit(onProgress, "verify-sts", "running");
392
+ try {
393
+ const ctx = await resolveEntityContext(checkpoint.companyUid, config.vaultConfig);
394
+ if (!ctx.credentials?.accessKeyId) {
395
+ throw new Error("STS vend returned empty credentials");
396
+ }
397
+ checkpoint.bucketName = ctx.bucketName;
398
+ checkpoint.completedSteps.push("verify-sts");
399
+ await writeCheckpoint(config.hqRoot, checkpoint);
400
+ emit(onProgress, "verify-sts", "done");
401
+ }
402
+ catch (err) {
403
+ checkpoint.failedStep = "verify-sts";
404
+ checkpoint.error = String(err);
405
+ await writeCheckpoint(config.hqRoot, checkpoint);
406
+ emit(onProgress, "verify-sts", "failed");
407
+ throw new StsVerifyError(`STS verification failed: ${err}`, err instanceof Error ? err : undefined);
408
+ }
409
+ }
410
+ else {
411
+ emit(onProgress, "verify-sts", "skipped", "Already complete");
412
+ }
413
+ // Step 5: First sync
414
+ if (!isStepComplete(checkpoint, "first-sync")) {
415
+ emit(onProgress, "first-sync", "running");
416
+ try {
417
+ await sync({
418
+ company: checkpoint.companyUid,
419
+ hqRoot: config.hqRoot,
420
+ vaultConfig: config.vaultConfig,
421
+ });
422
+ checkpoint.completedSteps.push("first-sync");
423
+ await writeCheckpoint(config.hqRoot, checkpoint);
424
+ emit(onProgress, "first-sync", "done");
425
+ }
426
+ catch (err) {
427
+ checkpoint.failedStep = "first-sync";
428
+ checkpoint.error = String(err);
429
+ await writeCheckpoint(config.hqRoot, checkpoint);
430
+ emit(onProgress, "first-sync", "failed");
431
+ throw new FirstSyncError(`First sync failed: ${err}`, err instanceof Error ? err : undefined);
432
+ }
433
+ }
434
+ else {
435
+ emit(onProgress, "first-sync", "skipped", "Already complete");
436
+ }
437
+ // Step 6: Write config
438
+ emit(onProgress, "write-config", "running");
439
+ // Get membership role from the accepted invite
440
+ const members = await client.listMembersOfCompany(checkpoint.companyUid);
441
+ const ours = members.find(m => m.personUid === checkpoint.personUid);
442
+ const role = ours?.role ?? "member";
443
+ const configPath = await writeHqConfig(config.hqRoot, {
444
+ companyUid: checkpoint.companyUid,
445
+ companySlug: checkpoint.companySlug ?? "",
446
+ personUid: checkpoint.personUid,
447
+ role,
448
+ bucketName: checkpoint.bucketName,
449
+ vaultApiUrl: config.vaultConfig.apiUrl,
450
+ configuredAt: new Date().toISOString(),
451
+ });
452
+ checkpoint.completedSteps.push("write-config");
453
+ await writeCheckpoint(config.hqRoot, checkpoint);
454
+ emit(onProgress, "write-config", "done", configPath);
455
+ // Clean up checkpoint on success
456
+ await deleteCheckpoint(config.hqRoot);
457
+ return {
458
+ personUid: checkpoint.personUid,
459
+ companyUid: checkpoint.companyUid,
460
+ companySlug: checkpoint.companySlug ?? "",
461
+ role,
462
+ bucketName: checkpoint.bucketName,
463
+ configPath,
464
+ };
465
+ }
466
+ /**
467
+ * Resume an interrupted onboarding flow from checkpoint.
468
+ */
469
+ export async function resumeOnboarding(config, onProgress) {
470
+ const checkpoint = await readCheckpoint(config.hqRoot);
471
+ if (!checkpoint) {
472
+ throw new Error("No onboarding checkpoint found. Run /onboard to start a new flow.");
473
+ }
474
+ // We need the original input to resume — checkpoint has enough state
475
+ // to reconstruct which flow we're in, but we need to synthesize input
476
+ if (checkpoint.mode === "create-company") {
477
+ // We can't fully reconstruct the original input from checkpoint alone,
478
+ // but the orchestrator is idempotent — completed steps will be skipped.
479
+ // For create-company, we need at minimum the company slug.
480
+ const input = {
481
+ mode: "create-company",
482
+ personName: "", // Not needed for resume — person already created
483
+ personEmail: "", // Not needed for resume
484
+ companyName: "", // Not needed for resume
485
+ companySlug: checkpoint.companySlug ?? "",
486
+ };
487
+ return createCompanyFlow(input, config, onProgress);
488
+ }
489
+ else {
490
+ const input = {
491
+ mode: "join-company",
492
+ personName: "",
493
+ personEmail: "",
494
+ inviteToken: checkpoint.inviteToken ?? "",
495
+ };
496
+ return joinCompanyFlow(input, config, onProgress);
497
+ }
498
+ }
499
+ /**
500
+ * Desktop installer contract implementation.
501
+ */
502
+ export const onboardingContract = {
503
+ async runOnboarding(input, config, onProgress) {
504
+ if (input.mode === "create-company") {
505
+ return createCompanyFlow(input, config, onProgress);
506
+ }
507
+ return joinCompanyFlow(input, config, onProgress);
508
+ },
509
+ resumeOnboarding,
510
+ };
511
+ // ---------------------------------------------------------------------------
512
+ // Helpers
513
+ // ---------------------------------------------------------------------------
514
+ function makeCheckpoint(mode) {
515
+ const now = new Date().toISOString();
516
+ return {
517
+ mode,
518
+ startedAt: now,
519
+ updatedAt: now,
520
+ completedSteps: [],
521
+ };
522
+ }
523
+ function emit(cb, step, status, detail) {
524
+ cb?.({ step, status, detail });
525
+ }
526
+ function slugFromEmail(email) {
527
+ return email.split("@")[0].toLowerCase().replace(/[^a-z0-9-]/g, "-");
528
+ }
529
+ async function safeGetEntity(client, uid) {
530
+ try {
531
+ return await client.entity.get(uid);
532
+ }
533
+ catch (err) {
534
+ if (err instanceof VaultNotFoundError)
535
+ return null;
536
+ throw err;
537
+ }
538
+ }
539
+ /**
540
+ * Look up an entity by slug, returning null on 404 instead of throwing.
541
+ *
542
+ * Used by createCompanyFlow / joinCompanyFlow to detect existing person and
543
+ * company entities BEFORE calling entity.create. This is defense-in-depth
544
+ * against the vault-service not enforcing slug uniqueness server-side — even
545
+ * if the server lets a duplicate create succeed, this guard ensures we never
546
+ * issue the create call in the first place when an entity with the same slug
547
+ * already exists.
548
+ */
549
+ async function safeFindBySlug(client, type, slug) {
550
+ try {
551
+ return await client.entity.findBySlug(type, slug);
552
+ }
553
+ catch (err) {
554
+ if (err instanceof VaultNotFoundError)
555
+ return null;
556
+ // Defense against the mocked-error case where the SDK wrapper isn't an
557
+ // instanceof match (mocks across module boundaries can break instanceof).
558
+ if (err instanceof Error && err.name === "VaultNotFoundError")
559
+ return null;
560
+ throw err;
561
+ }
562
+ }
563
+ async function writeHqConfig(hqRoot, config) {
564
+ const configPath = join(hqRoot, ".hq", "config.json");
565
+ await mkdir(dirname(configPath), { recursive: true });
566
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
567
+ return configPath;
568
+ }
569
+ //# sourceMappingURL=orchestrator.js.map
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Onboarding types (VLT-9 US-001; vendored from @indigoai-us/hq-onboarding).
3
+ *
4
+ * Discriminated union for the two onboarding paths (create vs join)
5
+ * plus progress events for the installer UI callback.
6
+ */
7
+ import type { VaultServiceConfig } from "@indigoai-us/hq-cloud";
8
+ export interface CreateCompanyInput {
9
+ mode: "create-company";
10
+ personName: string;
11
+ personEmail: string;
12
+ companyName: string;
13
+ companySlug: string;
14
+ }
15
+ export interface JoinCompanyInput {
16
+ mode: "join-company";
17
+ personName: string;
18
+ personEmail: string;
19
+ inviteToken: string;
20
+ }
21
+ export type OnboardingInput = CreateCompanyInput | JoinCompanyInput;
22
+ export interface OnboardingConfig {
23
+ vaultConfig: VaultServiceConfig;
24
+ /** Local HQ root directory for writing .hq/config.json */
25
+ hqRoot: string;
26
+ /** Stage name for resource tagging (e.g. "dev", "prod") */
27
+ stage?: string;
28
+ }
29
+ export type OnboardingStep = "create-person" | "create-company" | "provision-bucket" | "bootstrap-membership" | "verify-sts" | "write-config" | "parse-token" | "accept-invite" | "first-sync";
30
+ export type StepStatus = "pending" | "running" | "done" | "skipped" | "failed";
31
+ export interface OnboardingProgress {
32
+ step: OnboardingStep;
33
+ status: StepStatus;
34
+ detail?: string;
35
+ }
36
+ export type ProgressCallback = (progress: OnboardingProgress) => void;
37
+ export interface OnboardingResult {
38
+ personUid: string;
39
+ companyUid: string;
40
+ companySlug: string;
41
+ role: string;
42
+ bucketName?: string;
43
+ configPath: string;
44
+ }
45
+ export interface OnboardingCheckpoint {
46
+ mode: "create-company" | "join-company";
47
+ startedAt: string;
48
+ updatedAt: string;
49
+ personUid?: string;
50
+ companyUid?: string;
51
+ companySlug?: string;
52
+ bucketName?: string;
53
+ membershipKey?: string;
54
+ inviteToken?: string;
55
+ completedSteps: OnboardingStep[];
56
+ failedStep?: OnboardingStep;
57
+ error?: string;
58
+ }
59
+ /**
60
+ * DesktopInstallerContract — stable API boundary the desktop app's onboarding
61
+ * screen MUST conform to. The installer calls `runOnboarding()` with input +
62
+ * config + progress callback and gets back a typed result or error.
63
+ *
64
+ * Breaking changes to this interface require desktop team coordination.
65
+ */
66
+ export interface DesktopInstallerContract {
67
+ runOnboarding(input: OnboardingInput, config: OnboardingConfig, onProgress?: ProgressCallback): Promise<OnboardingResult>;
68
+ resumeOnboarding(config: OnboardingConfig, onProgress?: ProgressCallback): Promise<OnboardingResult>;
69
+ }
70
+ export interface HqConfig {
71
+ companyUid: string;
72
+ companySlug: string;
73
+ personUid: string;
74
+ role: string;
75
+ bucketName?: string;
76
+ vaultApiUrl: string;
77
+ configuredAt: string;
78
+ }
79
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Onboarding types (VLT-9 US-001; vendored from @indigoai-us/hq-onboarding).
3
+ *
4
+ * Discriminated union for the two onboarding paths (create vs join)
5
+ * plus progress events for the installer UI callback.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=types.js.map