@indigoai-us/hq-cloud 5.1.11 → 5.2.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/dist/bin/sync-runner.d.ts +1 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +19 -3
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +83 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/sync.d.ts +13 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +15 -4
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +36 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/cognito-auth.d.ts +2 -2
- package/dist/cognito-auth.d.ts.map +1 -1
- package/dist/cognito-auth.test.js +1 -1
- package/dist/cognito-auth.test.js.map +1 -1
- package/dist/context.d.ts +6 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +32 -13
- package/dist/context.js.map +1 -1
- package/dist/context.test.js +91 -1
- package/dist/context.test.js.map +1 -1
- package/dist/ignore.d.ts +1 -0
- package/dist/ignore.d.ts.map +1 -1
- package/dist/ignore.js +1 -1
- package/dist/ignore.js.map +1 -1
- package/dist/vault-client.d.ts +12 -0
- package/dist/vault-client.d.ts.map +1 -1
- package/dist/vault-client.js +22 -9
- package/dist/vault-client.js.map +1 -1
- package/dist/vault-client.test.js +60 -1
- package/dist/vault-client.test.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner.test.ts +108 -0
- package/src/bin/sync-runner.ts +30 -4
- package/src/cli/sync.test.ts +43 -0
- package/src/cli/sync.ts +29 -4
- package/src/cognito-auth.test.ts +1 -1
- package/src/cognito-auth.ts +2 -2
- package/src/context.test.ts +104 -1
- package/src/context.ts +46 -16
- package/src/ignore.ts +1 -1
- package/src/vault-client.test.ts +72 -0
- package/src/vault-client.ts +25 -7
package/src/context.ts
CHANGED
|
@@ -17,6 +17,13 @@ const DEFAULT_SESSION_DURATION_SECONDS = 900;
|
|
|
17
17
|
/** Cached contexts keyed by entity UID. */
|
|
18
18
|
const contextCache = new Map<string, EntityContext>();
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Closed-set of recognised entity-UID prefixes. Adding a new entity type
|
|
22
|
+
* means appending one entry here AND extending the dispatch in
|
|
23
|
+
* `resolveEntityContext` (cmp_ → /sts/vend, prs_ → /sts/vend-self).
|
|
24
|
+
*/
|
|
25
|
+
export const KNOWN_UID_PREFIXES = ["cmp_", "prs_"] as const;
|
|
26
|
+
|
|
20
27
|
/**
|
|
21
28
|
* Look up an entity by slug or UID via vault-service, then vend STS-scoped
|
|
22
29
|
* credentials for that entity. Returns an EntityContext ready for S3 ops.
|
|
@@ -34,9 +41,15 @@ export async function resolveEntityContext(
|
|
|
34
41
|
return cached;
|
|
35
42
|
}
|
|
36
43
|
|
|
37
|
-
// Step 1: Resolve entity — if it looks like a UID
|
|
38
|
-
// otherwise look up by slug
|
|
39
|
-
|
|
44
|
+
// Step 1: Resolve entity — if it looks like a known UID prefix, fetch directly;
|
|
45
|
+
// otherwise look up by slug. Explicit enumeration avoids over-matching slugs
|
|
46
|
+
// like foo_bar or team_alpha that happen to look like UIDs.
|
|
47
|
+
const looksLikeUid = KNOWN_UID_PREFIXES.some((p) =>
|
|
48
|
+
companyUidOrSlug.startsWith(p),
|
|
49
|
+
);
|
|
50
|
+
const looksLikePerson = companyUidOrSlug.startsWith("prs_");
|
|
51
|
+
|
|
52
|
+
const entity = looksLikeUid
|
|
40
53
|
? await fetchEntity(companyUidOrSlug, config)
|
|
41
54
|
: await fetchEntityBySlug("company", companyUidOrSlug, config);
|
|
42
55
|
|
|
@@ -47,8 +60,13 @@ export async function resolveEntityContext(
|
|
|
47
60
|
);
|
|
48
61
|
}
|
|
49
62
|
|
|
50
|
-
// Step 2:
|
|
51
|
-
|
|
63
|
+
// Step 2: Dispatch credential vending by UID prefix.
|
|
64
|
+
// cmp_* → POST /sts/vend (company path; membership-gated)
|
|
65
|
+
// prs_* → POST /sts/vend-self (person path; self-ownership-gated)
|
|
66
|
+
// slug → POST /sts/vend (legacy slug path is company-only)
|
|
67
|
+
const vendResult = looksLikePerson
|
|
68
|
+
? await vendSelfCredentials(entity.uid, config)
|
|
69
|
+
: await vendCredentials(entity.uid, config);
|
|
52
70
|
|
|
53
71
|
const ctx: EntityContext = {
|
|
54
72
|
uid: entity.uid,
|
|
@@ -153,26 +171,38 @@ async function fetchEntityBySlug(
|
|
|
153
171
|
return data.entity;
|
|
154
172
|
}
|
|
155
173
|
|
|
156
|
-
async function
|
|
157
|
-
|
|
174
|
+
async function postVend(
|
|
175
|
+
route: string,
|
|
176
|
+
body: Record<string, unknown>,
|
|
158
177
|
config: VaultServiceConfig,
|
|
159
178
|
): Promise<VendResponse> {
|
|
160
|
-
const res = await fetch(`${config.apiUrl}
|
|
179
|
+
const res = await fetch(`${config.apiUrl}${route}`, {
|
|
161
180
|
method: "POST",
|
|
162
181
|
headers: {
|
|
163
182
|
"Content-Type": "application/json",
|
|
164
183
|
Authorization: `Bearer ${config.authToken}`,
|
|
165
184
|
},
|
|
166
|
-
body: JSON.stringify({
|
|
167
|
-
companyUid,
|
|
168
|
-
durationSeconds: DEFAULT_SESSION_DURATION_SECONDS,
|
|
169
|
-
}),
|
|
185
|
+
body: JSON.stringify({ ...body, durationSeconds: DEFAULT_SESSION_DURATION_SECONDS }),
|
|
170
186
|
});
|
|
171
187
|
if (!res.ok) {
|
|
172
|
-
const
|
|
173
|
-
throw new Error(
|
|
174
|
-
`STS vend failed for ${companyUid}: ${res.status} ${body}`,
|
|
175
|
-
);
|
|
188
|
+
const text = await res.text();
|
|
189
|
+
throw new Error(`STS ${route} failed: ${res.status} ${text}`);
|
|
176
190
|
}
|
|
177
191
|
return (await res.json()) as VendResponse;
|
|
178
192
|
}
|
|
193
|
+
|
|
194
|
+
async function vendCredentials(
|
|
195
|
+
companyUid: string,
|
|
196
|
+
config: VaultServiceConfig,
|
|
197
|
+
): Promise<VendResponse> {
|
|
198
|
+
return postVend("/sts/vend", { companyUid }, config);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function vendSelfCredentials(
|
|
202
|
+
personUid: string,
|
|
203
|
+
config: VaultServiceConfig,
|
|
204
|
+
): Promise<VendResponse> {
|
|
205
|
+
// Use `+` concat at the literal so guard 5 (grep "/sts/vend-self") still finds it
|
|
206
|
+
const route = "/sts/vend-self";
|
|
207
|
+
return postVend(route, { personUid }, config);
|
|
208
|
+
}
|
package/src/ignore.ts
CHANGED
package/src/vault-client.test.ts
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
VaultNotFoundError,
|
|
13
13
|
VaultConflictError,
|
|
14
14
|
VaultClientError,
|
|
15
|
+
pickCanonicalPersonEntity,
|
|
16
|
+
type EntityInfo,
|
|
15
17
|
} from "./vault-client.js";
|
|
16
18
|
|
|
17
19
|
// ---------------------------------------------------------------------------
|
|
@@ -620,4 +622,74 @@ describe("VaultClient identity bootstrap", () => {
|
|
|
620
622
|
expect(person.uid).toBe("prs_a");
|
|
621
623
|
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
622
624
|
});
|
|
625
|
+
|
|
626
|
+
it("pickCanonicalPersonEntity_picks_oldest_tiebreak_uid", () => {
|
|
627
|
+
const list: EntityInfo[] = [
|
|
628
|
+
{ uid: "prs_b", slug: "b", type: "person", status: "active", createdAt: "2026-01-02T00:00:00Z" } as EntityInfo,
|
|
629
|
+
{ uid: "prs_a", slug: "a", type: "person", status: "active", createdAt: "2026-01-01T00:00:00Z" } as EntityInfo,
|
|
630
|
+
];
|
|
631
|
+
// Older createdAt wins
|
|
632
|
+
expect(pickCanonicalPersonEntity(list)?.uid).toBe("prs_a");
|
|
633
|
+
|
|
634
|
+
// Same createdAt: uid tiebreak (lexicographic ascending)
|
|
635
|
+
const tieList: EntityInfo[] = [
|
|
636
|
+
{ uid: "prs_z", slug: "z", type: "person", status: "active", createdAt: "2026-01-01T00:00:00Z" } as EntityInfo,
|
|
637
|
+
{ uid: "prs_a", slug: "a", type: "person", status: "active", createdAt: "2026-01-01T00:00:00Z" } as EntityInfo,
|
|
638
|
+
];
|
|
639
|
+
expect(pickCanonicalPersonEntity(tieList)?.uid).toBe("prs_a");
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it("pickCanonicalPersonEntity_handles_missing_createdAt_deterministically", () => {
|
|
643
|
+
// undefined createdAt coalesces to "" which sorts before any ISO date string.
|
|
644
|
+
// So the entity with undefined createdAt wins on the createdAt comparison.
|
|
645
|
+
// When two entities both lack createdAt, uid tiebreak applies.
|
|
646
|
+
const list: EntityInfo[] = [
|
|
647
|
+
{ uid: "prs_b", slug: "b", type: "person", status: "active", createdAt: "2026-01-01T00:00:00Z" } as EntityInfo,
|
|
648
|
+
{ uid: "prs_a", slug: "a", type: "person", status: "active" } as EntityInfo, // no createdAt
|
|
649
|
+
];
|
|
650
|
+
// "" < "2026-..." so prs_a (undefined→"") wins
|
|
651
|
+
expect(pickCanonicalPersonEntity(list)?.uid).toBe("prs_a");
|
|
652
|
+
|
|
653
|
+
// Both undefined: uid tiebreak
|
|
654
|
+
const bothUndefined: EntityInfo[] = [
|
|
655
|
+
{ uid: "prs_z", slug: "z", type: "person", status: "active" } as EntityInfo,
|
|
656
|
+
{ uid: "prs_a", slug: "a", type: "person", status: "active" } as EntityInfo,
|
|
657
|
+
];
|
|
658
|
+
expect(pickCanonicalPersonEntity(bothUndefined)?.uid).toBe("prs_a");
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it("pickCanonicalPersonEntity_filters_out_non_person_types", () => {
|
|
662
|
+
const mixed: EntityInfo[] = [
|
|
663
|
+
{ uid: "cmp_x", slug: "x", type: "company", status: "active", createdAt: "2025-01-01T00:00:00Z" } as EntityInfo,
|
|
664
|
+
{ uid: "prs_b", slug: "b", type: "person", status: "active", createdAt: "2026-01-02T00:00:00Z" } as EntityInfo,
|
|
665
|
+
];
|
|
666
|
+
const result = pickCanonicalPersonEntity(mixed);
|
|
667
|
+
expect(result?.uid).toBe("prs_b");
|
|
668
|
+
expect(result?.type).toBe("person");
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it("vendSelf_roundtrip", async () => {
|
|
672
|
+
fetchSpy.mockResolvedValueOnce(
|
|
673
|
+
jsonResponse(200, {
|
|
674
|
+
credentials: {
|
|
675
|
+
accessKeyId: "AKIAIOSFODNN7EXAMPLE",
|
|
676
|
+
secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
|
677
|
+
sessionToken: "FwoGZXIvYXdzEBY...",
|
|
678
|
+
},
|
|
679
|
+
expiresAt: "2026-01-01T01:00:00.000Z",
|
|
680
|
+
}),
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
const result = await client.sts.vendSelf({ personUid: "prs_x" });
|
|
684
|
+
|
|
685
|
+
expect(result.credentials.accessKeyId).toBe("AKIAIOSFODNN7EXAMPLE");
|
|
686
|
+
expect(result.credentials.secretAccessKey).toBe("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY");
|
|
687
|
+
expect(result.credentials.sessionToken).toBe("FwoGZXIvYXdzEBY...");
|
|
688
|
+
expect(typeof result.expiresAt).toBe("string");
|
|
689
|
+
|
|
690
|
+
const [url, init] = fetchSpy.mock.calls[0] as [string, RequestInit];
|
|
691
|
+
expect(url).toBe("https://vault.test.example.com/sts/vend-self");
|
|
692
|
+
expect((init.method as string).toUpperCase()).toBe("POST");
|
|
693
|
+
expect(JSON.parse(init.body as string)).toEqual({ personUid: "prs_x" });
|
|
694
|
+
});
|
|
623
695
|
});
|
package/src/vault-client.ts
CHANGED
|
@@ -112,6 +112,23 @@ export interface EntityInfo {
|
|
|
112
112
|
createdAt: string;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
export function pickCanonicalPersonEntity(
|
|
116
|
+
list: EntityInfo[],
|
|
117
|
+
): EntityInfo | null {
|
|
118
|
+
// Defensive filter — callers today pass `entity.listByType("person")` so this is
|
|
119
|
+
// a no-op, but a future caller passing a mixed list would otherwise silently get
|
|
120
|
+
// back a non-person entity.
|
|
121
|
+
const persons = list.filter((e) => e.type === "person");
|
|
122
|
+
if (persons.length === 0) return null;
|
|
123
|
+
const sorted = [...persons].sort((a, b) => {
|
|
124
|
+
const ac = (a.createdAt as string | undefined) ?? "";
|
|
125
|
+
const bc = (b.createdAt as string | undefined) ?? "";
|
|
126
|
+
if (ac !== bc) return ac < bc ? -1 : 1;
|
|
127
|
+
return a.uid < b.uid ? -1 : 1;
|
|
128
|
+
});
|
|
129
|
+
return sorted[0];
|
|
130
|
+
}
|
|
131
|
+
|
|
115
132
|
export interface PendingInviteByEmail {
|
|
116
133
|
membershipKey: string;
|
|
117
134
|
companyUid: string;
|
|
@@ -344,13 +361,8 @@ export class VaultClient {
|
|
|
344
361
|
displayName: string;
|
|
345
362
|
}): Promise<EntityInfo> {
|
|
346
363
|
const existing = await this.entity.listByType("person");
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
const bc = (b.createdAt as string | undefined) ?? "";
|
|
350
|
-
if (ac !== bc) return ac < bc ? -1 : 1;
|
|
351
|
-
return a.uid < b.uid ? -1 : 1;
|
|
352
|
-
});
|
|
353
|
-
if (sorted.length > 0) return sorted[0];
|
|
364
|
+
const pick = pickCanonicalPersonEntity(existing);
|
|
365
|
+
if (pick !== null) return pick;
|
|
354
366
|
|
|
355
367
|
const slug =
|
|
356
368
|
hints.displayName
|
|
@@ -396,6 +408,12 @@ export class VaultClient {
|
|
|
396
408
|
const data = await this.post<VendChildResult>("/sts/vend-child", input);
|
|
397
409
|
return data;
|
|
398
410
|
},
|
|
411
|
+
vendSelf: async (input: { personUid: string; durationSeconds?: number }): Promise<{
|
|
412
|
+
credentials: { accessKeyId: string; secretAccessKey: string; sessionToken: string };
|
|
413
|
+
expiresAt: string;
|
|
414
|
+
}> => {
|
|
415
|
+
return this.post("/sts/vend-self", input);
|
|
416
|
+
},
|
|
399
417
|
};
|
|
400
418
|
|
|
401
419
|
// -- HTTP primitives with retry -------------------------------------------
|