@indigoai-us/hq-cloud 6.15.0 → 6.15.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.
- package/dist/bin/sync-mutation.d.ts +16 -0
- package/dist/bin/sync-mutation.d.ts.map +1 -0
- package/dist/bin/sync-mutation.js +60 -0
- package/dist/bin/sync-mutation.js.map +1 -0
- package/dist/bin/sync-mutation.test.d.ts +2 -0
- package/dist/bin/sync-mutation.test.d.ts.map +1 -0
- package/dist/bin/sync-mutation.test.js +165 -0
- package/dist/bin/sync-mutation.test.js.map +1 -0
- package/dist/bin/sync-runner-company.d.ts +8 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +16 -0
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-company.test.d.ts +2 -0
- package/dist/bin/sync-runner-company.test.d.ts.map +1 -0
- package/dist/bin/sync-runner-company.test.js +36 -0
- package/dist/bin/sync-runner-company.test.js.map +1 -0
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +98 -8
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +17 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +109 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/conflict-recovery.test.d.ts +2 -0
- package/dist/cli/conflict-recovery.test.d.ts.map +1 -0
- package/dist/cli/conflict-recovery.test.js +201 -0
- package/dist/cli/conflict-recovery.test.js.map +1 -0
- package/dist/cli/conflict.d.ts +60 -0
- package/dist/cli/conflict.d.ts.map +1 -1
- package/dist/cli/conflict.js +333 -0
- package/dist/cli/conflict.js.map +1 -1
- package/dist/cli/sync.d.ts +35 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +100 -0
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +85 -1
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/skill-telemetry.d.ts +6 -0
- package/dist/skill-telemetry.d.ts.map +1 -1
- package/dist/skill-telemetry.js +14 -2
- package/dist/skill-telemetry.js.map +1 -1
- package/dist/skill-telemetry.test.js +79 -0
- package/dist/skill-telemetry.test.js.map +1 -1
- package/dist/sync/candidate-uploader.d.ts +88 -0
- package/dist/sync/candidate-uploader.d.ts.map +1 -0
- package/dist/sync/candidate-uploader.js +212 -0
- package/dist/sync/candidate-uploader.js.map +1 -0
- package/dist/sync/candidate-uploader.test.d.ts +2 -0
- package/dist/sync/candidate-uploader.test.d.ts.map +1 -0
- package/dist/sync/candidate-uploader.test.js +132 -0
- package/dist/sync/candidate-uploader.test.js.map +1 -0
- package/dist/sync/delta-client.d.ts +73 -0
- package/dist/sync/delta-client.d.ts.map +1 -0
- package/dist/sync/delta-client.js +201 -0
- package/dist/sync/delta-client.js.map +1 -0
- package/dist/sync/delta-client.test.d.ts +2 -0
- package/dist/sync/delta-client.test.d.ts.map +1 -0
- package/dist/sync/delta-client.test.js +97 -0
- package/dist/sync/delta-client.test.js.map +1 -0
- package/dist/sync/durable-apply.d.ts +76 -0
- package/dist/sync/durable-apply.d.ts.map +1 -0
- package/dist/sync/durable-apply.js +530 -0
- package/dist/sync/durable-apply.js.map +1 -0
- package/dist/sync/durable-apply.test.d.ts +2 -0
- package/dist/sync/durable-apply.test.d.ts.map +1 -0
- package/dist/sync/durable-apply.test.js +180 -0
- package/dist/sync/durable-apply.test.js.map +1 -0
- package/dist/sync/event-sync.d.ts +33 -1
- package/dist/sync/event-sync.d.ts.map +1 -1
- package/dist/sync/event-sync.js +149 -1
- package/dist/sync/event-sync.js.map +1 -1
- package/dist/sync/event-sync.test.js +142 -1
- package/dist/sync/event-sync.test.js.map +1 -1
- package/dist/sync/index.d.ts +2 -0
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +1 -0
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/multipart-uploader.d.ts +99 -0
- package/dist/sync/multipart-uploader.d.ts.map +1 -0
- package/dist/sync/multipart-uploader.js +447 -0
- package/dist/sync/multipart-uploader.js.map +1 -0
- package/dist/sync/multipart-uploader.test.d.ts +2 -0
- package/dist/sync/multipart-uploader.test.d.ts.map +1 -0
- package/dist/sync/multipart-uploader.test.js +119 -0
- package/dist/sync/multipart-uploader.test.js.map +1 -0
- package/dist/sync/mutation-client.d.ts +85 -0
- package/dist/sync/mutation-client.d.ts.map +1 -0
- package/dist/sync/mutation-client.js +245 -0
- package/dist/sync/mutation-client.js.map +1 -0
- package/dist/sync/mutation-client.test.d.ts +2 -0
- package/dist/sync/mutation-client.test.d.ts.map +1 -0
- package/dist/sync/mutation-client.test.js +51 -0
- package/dist/sync/mutation-client.test.js.map +1 -0
- package/dist/sync/push-receiver.d.ts +45 -0
- package/dist/sync/push-receiver.d.ts.map +1 -1
- package/dist/sync/push-receiver.js +101 -0
- package/dist/sync/push-receiver.js.map +1 -1
- package/dist/sync/push-receiver.test.js +54 -2
- package/dist/sync/push-receiver.test.js.map +1 -1
- package/dist/sync/scope-inventory-client.d.ts +69 -0
- package/dist/sync/scope-inventory-client.d.ts.map +1 -0
- package/dist/sync/scope-inventory-client.js +210 -0
- package/dist/sync/scope-inventory-client.js.map +1 -0
- package/dist/sync/scope-inventory-client.test.d.ts +2 -0
- package/dist/sync/scope-inventory-client.test.d.ts.map +1 -0
- package/dist/sync/scope-inventory-client.test.js +94 -0
- package/dist/sync/scope-inventory-client.test.js.map +1 -0
- package/dist/sync/snapshot-client.d.ts +98 -0
- package/dist/sync/snapshot-client.d.ts.map +1 -0
- package/dist/sync/snapshot-client.js +402 -0
- package/dist/sync/snapshot-client.js.map +1 -0
- package/dist/sync/snapshot-client.test.d.ts +2 -0
- package/dist/sync/snapshot-client.test.d.ts.map +1 -0
- package/dist/sync/snapshot-client.test.js +169 -0
- package/dist/sync/snapshot-client.test.js.map +1 -0
- package/dist/sync/uploader-finalization.d.ts +97 -0
- package/dist/sync/uploader-finalization.d.ts.map +1 -0
- package/dist/sync/uploader-finalization.js +273 -0
- package/dist/sync/uploader-finalization.js.map +1 -0
- package/dist/sync/uploader-finalization.test.d.ts +2 -0
- package/dist/sync/uploader-finalization.test.d.ts.map +1 -0
- package/dist/sync/uploader-finalization.test.js +92 -0
- package/dist/sync/uploader-finalization.test.js.map +1 -0
- package/dist/telemetry.d.ts +11 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +21 -2
- package/dist/telemetry.js.map +1 -1
- package/dist/telemetry.test.js +80 -0
- package/dist/telemetry.test.js.map +1 -1
- package/package.json +6 -1
- package/.claude/policies/hq-cloud-esm-cannot-spy-fs-builtins.md +0 -30
- package/.claude/policies/hq-cloud-strip-types-no-parameter-properties.md +0 -22
- package/.github/workflows/ci.yml +0 -84
- package/.github/workflows/publish.yml +0 -56
- package/.github/workflows/unreleased-commits-nag.yml +0 -256
- package/eslint.config.js +0 -67
- package/pnpm-workspace.yaml +0 -2
- package/scripts/presign-transport-e2e.mjs +0 -250
- package/scripts/vault-rebaseline.sh +0 -323
- package/scripts/vault-rescue.sh +0 -332
- package/src/active-company.test.ts +0 -188
- package/src/active-company.ts +0 -168
- package/src/agent-codex-instructions.test.ts +0 -332
- package/src/agent-codex-instructions.ts +0 -309
- package/src/auth.ts +0 -146
- package/src/backup-prune.test.ts +0 -98
- package/src/backup-prune.ts +0 -182
- package/src/bin/backup-prune-runner.ts +0 -33
- package/src/bin/rescue-runner.ts +0 -25
- package/src/bin/sync-runner-company.ts +0 -695
- package/src/bin/sync-runner-events.test.ts +0 -143
- package/src/bin/sync-runner-events.ts +0 -55
- package/src/bin/sync-runner-planning.test.ts +0 -311
- package/src/bin/sync-runner-planning.ts +0 -258
- package/src/bin/sync-runner-rollup.test.ts +0 -37
- package/src/bin/sync-runner-rollup.ts +0 -97
- package/src/bin/sync-runner-telemetry.ts +0 -15
- package/src/bin/sync-runner-watch-loop.ts +0 -1235
- package/src/bin/sync-runner-watch-routes.test.ts +0 -71
- package/src/bin/sync-runner-watch-routes.ts +0 -184
- package/src/bin/sync-runner.test.ts +0 -8767
- package/src/bin/sync-runner.ts +0 -2190
- package/src/cli/accept.ts +0 -124
- package/src/cli/conflict.ts +0 -119
- package/src/cli/doctor.test.ts +0 -581
- package/src/cli/doctor.ts +0 -642
- package/src/cli/index.ts +0 -49
- package/src/cli/invite.test.ts +0 -250
- package/src/cli/invite.ts +0 -214
- package/src/cli/promote.ts +0 -157
- package/src/cli/reindex-knowledge.test.ts +0 -307
- package/src/cli/reindex-knowledge.ts +0 -450
- package/src/cli/reindex.test.ts +0 -957
- package/src/cli/reindex.ts +0 -979
- package/src/cli/rescue-classify-ordering.test.ts +0 -548
- package/src/cli/rescue-clone-diagnostics.test.ts +0 -120
- package/src/cli/rescue-core.ts +0 -3011
- package/src/cli/rescue-drift-reconcile.test.ts +0 -179
- package/src/cli/rescue-drop-dir-symlink.test.ts +0 -224
- package/src/cli/rescue-exec-bit-preserve.test.ts +0 -187
- package/src/cli/rescue-hq-root-guard.test.ts +0 -232
- package/src/cli/rescue-journal-reconcile.test.ts +0 -215
- package/src/cli/rescue-mtime-preserve.test.ts +0 -203
- package/src/cli/rescue-settings-reconcile.test.ts +0 -637
- package/src/cli/rescue-snapshot.test.ts +0 -57
- package/src/cli/rescue-snapshot.ts +0 -51
- package/src/cli/rescue.reindex.test.ts +0 -63
- package/src/cli/rescue.test.ts +0 -131
- package/src/cli/rescue.ts +0 -182
- package/src/cli/share.test.ts +0 -7843
- package/src/cli/share.ts +0 -3663
- package/src/cli/sync-scope.test.ts +0 -652
- package/src/cli/sync.test.ts +0 -5207
- package/src/cli/sync.ts +0 -3470
- package/src/cli/tombstones.ts +0 -106
- package/src/cli/watch-event-push-conflict.test.ts +0 -234
- package/src/client-info.test.ts +0 -214
- package/src/client-info.ts +0 -121
- package/src/cognito-auth.test.ts +0 -712
- package/src/cognito-auth.ts +0 -1422
- package/src/company-resolver.test.ts +0 -618
- package/src/company-resolver.ts +0 -521
- package/src/context.test.ts +0 -583
- package/src/context.ts +0 -378
- package/src/daemon-worker.ts +0 -26
- package/src/daemon.ts +0 -99
- package/src/entity-resolver.test.ts +0 -315
- package/src/entity-resolver.ts +0 -180
- package/src/ignore.test.ts +0 -466
- package/src/ignore.ts +0 -469
- package/src/index.ts +0 -439
- package/src/journal.test.ts +0 -968
- package/src/journal.ts +0 -765
- package/src/lib/cloud-authoritative.test.ts +0 -45
- package/src/lib/cloud-authoritative.ts +0 -59
- package/src/lib/conflict-file.ts +0 -86
- package/src/lib/conflict-index.ts +0 -289
- package/src/lib/conflict.test.ts +0 -348
- package/src/lib/describe-error.test.ts +0 -100
- package/src/lib/describe-error.ts +0 -58
- package/src/lib/exit-codes.ts +0 -24
- package/src/lib/machine-id.test.ts +0 -231
- package/src/lib/machine-id.ts +0 -175
- package/src/lib/net-errors.test.ts +0 -65
- package/src/lib/net-errors.ts +0 -86
- package/src/lib/readlink-safe.test.ts +0 -43
- package/src/lib/readlink-safe.ts +0 -29
- package/src/local-path-codec.test.ts +0 -138
- package/src/local-path-codec.ts +0 -161
- package/src/machine-auth.test.ts +0 -1323
- package/src/manifest-reconcile.test.ts +0 -1123
- package/src/manifest-reconcile.ts +0 -518
- package/src/object-io.test.ts +0 -1221
- package/src/object-io.ts +0 -1306
- package/src/operation-lock.test.ts +0 -484
- package/src/operation-lock.ts +0 -680
- package/src/outcome-telemetry.test.ts +0 -498
- package/src/outcome-telemetry.ts +0 -639
- package/src/personal-vault-exclusions.test.ts +0 -308
- package/src/personal-vault-exclusions.ts +0 -354
- package/src/personal-vault.test.ts +0 -756
- package/src/personal-vault.ts +0 -496
- package/src/prefix-coalesce.test.ts +0 -240
- package/src/prefix-coalesce.ts +0 -273
- package/src/public-surface.test.ts +0 -117
- package/src/qmd-reindex.test.ts +0 -877
- package/src/qmd-reindex.ts +0 -842
- package/src/read-only-state-dir.test.ts +0 -188
- package/src/remote-pull.test.ts +0 -1130
- package/src/remote-pull.ts +0 -618
- package/src/s3.symlink-materialize.test.ts +0 -492
- package/src/s3.test.ts +0 -1789
- package/src/s3.ts +0 -1532
- package/src/schemas/signal-types.test.ts +0 -82
- package/src/schemas/signal-types.ts +0 -38
- package/src/schemas/source-channels.test.ts +0 -82
- package/src/schemas/source-channels.ts +0 -53
- package/src/scope-shrink.test.ts +0 -633
- package/src/scope-shrink.ts +0 -481
- package/src/signals/get.test.ts +0 -310
- package/src/signals/get.ts +0 -75
- package/src/signals/internals.ts +0 -195
- package/src/signals/list.test.ts +0 -420
- package/src/signals/list.ts +0 -79
- package/src/signals/parse.ts +0 -8
- package/src/signals/types.ts +0 -91
- package/src/skill-telemetry.test.ts +0 -1825
- package/src/skill-telemetry.ts +0 -1439
- package/src/sources/get.test.ts +0 -293
- package/src/sources/get.ts +0 -66
- package/src/sources/internals.ts +0 -198
- package/src/sources/list.test.ts +0 -402
- package/src/sources/list.ts +0 -84
- package/src/sources/parse.ts +0 -43
- package/src/sources/types.ts +0 -84
- package/src/sync/event-sync.test.ts +0 -594
- package/src/sync/event-sync.ts +0 -545
- package/src/sync/feature-flags.test.ts +0 -378
- package/src/sync/feature-flags.ts +0 -62
- package/src/sync/index.ts +0 -76
- package/src/sync/lease-client.test.ts +0 -128
- package/src/sync/lease-client.ts +0 -207
- package/src/sync/logger.test.ts +0 -242
- package/src/sync/logger.ts +0 -79
- package/src/sync/metrics.test.ts +0 -462
- package/src/sync/metrics.ts +0 -213
- package/src/sync/pull-scope.ts +0 -265
- package/src/sync/push-event.test.ts +0 -266
- package/src/sync/push-event.ts +0 -224
- package/src/sync/push-receiver.test.ts +0 -566
- package/src/sync/push-receiver.ts +0 -1048
- package/src/sync/push-transport.ts +0 -231
- package/src/sync/realtime-rollout.test.ts +0 -86
- package/src/sync/realtime-rollout.ts +0 -262
- package/src/sync/state-store.test.ts +0 -194
- package/src/sync/state-store.ts +0 -727
- package/src/sync-core.ts +0 -58
- package/src/sync-progress.test.ts +0 -94
- package/src/sync-progress.ts +0 -140
- package/src/telemetry-events.test.ts +0 -88
- package/src/telemetry-events.ts +0 -205
- package/src/telemetry.test.ts +0 -1280
- package/src/telemetry.ts +0 -1109
- package/src/types.ts +0 -314
- package/src/vault-client.test.ts +0 -1380
- package/src/vault-client.ts +0 -1694
- package/src/version.ts +0 -24
- package/src/watch-roots.test.ts +0 -278
- package/src/watch-roots.ts +0 -162
- package/src/watcher-event-gate.test.ts +0 -212
- package/src/watcher.test.ts +0 -1079
- package/src/watcher.ts +0 -1741
- package/test/e2e/sync/cross-tenant-isolation.test.ts +0 -630
- package/test/e2e/sync/skill-telemetry-oversized-transcript.test.ts +0 -124
- package/test/e2e/sync/transient-company-leg.test.ts +0 -384
- package/test/e2e/sync/windows-unreadable-link-leg.test.ts +0 -191
- package/test/e2e/watcher-real-chokidar.test.ts +0 -165
- package/test/e2e/watcher-recursive-backend.test.ts +0 -181
- package/test/e2e/watcher-scoped-coverage.test.ts +0 -381
- package/test/invite-flow.integration.test.ts +0 -244
- package/test/joiner-manifest-reconcile.integration.test.ts +0 -322
- package/test/share-sync.integration.test.ts +0 -213
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -22
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for entity-resolver.ts — slug → EntityContext resolution.
|
|
3
|
-
*
|
|
4
|
-
* Mocks globalThis.fetch to simulate vault-service responses for:
|
|
5
|
-
* GET /membership/me → list memberships
|
|
6
|
-
* GET /entity/{uid} → entity info (slug, bucketName)
|
|
7
|
-
* POST /entities/{uid}/sts → STS credentials
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
11
|
-
import { clearContextCache } from "./context.js";
|
|
12
|
-
import {
|
|
13
|
-
resolveEntity,
|
|
14
|
-
listAvailableEntities,
|
|
15
|
-
EntityNotFoundError,
|
|
16
|
-
EntityPermissionError,
|
|
17
|
-
EntityResolutionError,
|
|
18
|
-
} from "./entity-resolver.js";
|
|
19
|
-
import type { VaultServiceConfig } from "./types.js";
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// Test fixtures
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
const VAULT_CONFIG: VaultServiceConfig = {
|
|
26
|
-
apiUrl: "https://vault.test",
|
|
27
|
-
authToken: "test-jwt",
|
|
28
|
-
region: "us-east-1",
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const ENTITIES = {
|
|
32
|
-
indigo: {
|
|
33
|
-
uid: "cmp_indigo_001",
|
|
34
|
-
slug: "indigo",
|
|
35
|
-
type: "company",
|
|
36
|
-
name: "Indigo",
|
|
37
|
-
bucketName: "hq-vault-indigo",
|
|
38
|
-
status: "active",
|
|
39
|
-
},
|
|
40
|
-
personal: {
|
|
41
|
-
uid: "cmp_personal_001",
|
|
42
|
-
slug: "personal",
|
|
43
|
-
type: "person",
|
|
44
|
-
name: "Stefan",
|
|
45
|
-
bucketName: "hq-vault-personal",
|
|
46
|
-
status: "active",
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const STS_CREDS = {
|
|
51
|
-
credentials: {
|
|
52
|
-
accessKeyId: "ASIAMOCK000000000001",
|
|
53
|
-
secretAccessKey: "mockSecret",
|
|
54
|
-
sessionToken: "mockSession",
|
|
55
|
-
},
|
|
56
|
-
expiresAt: new Date(Date.now() + 3600_000).toISOString(),
|
|
57
|
-
bucketName: "hq-vault-indigo",
|
|
58
|
-
region: "us-east-1",
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
// Mock helpers
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
|
|
65
|
-
function json(body: unknown, status = 200): Response {
|
|
66
|
-
return new Response(JSON.stringify(body), {
|
|
67
|
-
status,
|
|
68
|
-
headers: { "Content-Type": "application/json" },
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
interface MockOptions {
|
|
73
|
-
memberships?: Array<{ companyUid: string; role: string }>;
|
|
74
|
-
entities?: Record<string, typeof ENTITIES.indigo>;
|
|
75
|
-
stsStatus?: number;
|
|
76
|
-
stsBody?: unknown;
|
|
77
|
-
entityStatus?: number;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function setupFetchMock(opts: MockOptions = {}) {
|
|
81
|
-
const memberships = opts.memberships ?? [
|
|
82
|
-
{ companyUid: "cmp_indigo_001", role: "owner" },
|
|
83
|
-
{ companyUid: "cmp_personal_001", role: "owner" },
|
|
84
|
-
];
|
|
85
|
-
|
|
86
|
-
const entities = opts.entities ?? {
|
|
87
|
-
cmp_indigo_001: ENTITIES.indigo,
|
|
88
|
-
cmp_personal_001: ENTITIES.personal,
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const fetchMock = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
|
|
92
|
-
const url = typeof input === "string" ? input : input.toString();
|
|
93
|
-
const method = (init?.method ?? "GET").toUpperCase();
|
|
94
|
-
|
|
95
|
-
// GET /membership/me
|
|
96
|
-
if (method === "GET" && url.endsWith("/membership/me")) {
|
|
97
|
-
return json({
|
|
98
|
-
memberships: memberships.map((m) => ({
|
|
99
|
-
membershipKey: `mbr_${m.companyUid}`,
|
|
100
|
-
personUid: "prs_test_001",
|
|
101
|
-
companyUid: m.companyUid,
|
|
102
|
-
role: m.role,
|
|
103
|
-
status: "active",
|
|
104
|
-
invitedBy: "system",
|
|
105
|
-
invitedAt: "2026-01-01T00:00:00Z",
|
|
106
|
-
createdAt: "2026-01-01T00:00:00Z",
|
|
107
|
-
updatedAt: "2026-01-01T00:00:00Z",
|
|
108
|
-
})),
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// GET /entity/{uid}
|
|
113
|
-
const entityGetMatch = /\/entity\/([^/?]+)$/.exec(url);
|
|
114
|
-
if (method === "GET" && entityGetMatch) {
|
|
115
|
-
const uid = decodeURIComponent(entityGetMatch[1]);
|
|
116
|
-
const entity = entities[uid as keyof typeof entities];
|
|
117
|
-
if (!entity || opts.entityStatus === 404) {
|
|
118
|
-
return json({ error: "Not found" }, opts.entityStatus ?? 404);
|
|
119
|
-
}
|
|
120
|
-
return json({ entity });
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// POST /sts/vend (canonical vault-service endpoint; was /entities/{uid}/sts
|
|
124
|
-
// in early drafts but that path doesn't actually exist server-side — the
|
|
125
|
-
// resolver now delegates to resolveEntityContext() which uses this route).
|
|
126
|
-
if (method === "POST" && url.endsWith("/sts/vend")) {
|
|
127
|
-
if (opts.stsStatus && opts.stsStatus >= 400) {
|
|
128
|
-
const body = opts.stsBody ?? { error: `STS error ${opts.stsStatus}` };
|
|
129
|
-
return json(body, opts.stsStatus);
|
|
130
|
-
}
|
|
131
|
-
// resolveEntityContext also re-fetches /entity/{uid} before vending, so
|
|
132
|
-
// the test mock above already handles that. Return creds + expiresAt;
|
|
133
|
-
// bucket/region come from the entity object + config respectively.
|
|
134
|
-
return json(opts.stsBody ?? {
|
|
135
|
-
credentials: STS_CREDS.credentials,
|
|
136
|
-
expiresAt: STS_CREDS.expiresAt,
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return json({ error: `Unhandled: ${method} ${url}` }, 500);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
144
|
-
return fetchMock;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// ---------------------------------------------------------------------------
|
|
148
|
-
// Tests
|
|
149
|
-
// ---------------------------------------------------------------------------
|
|
150
|
-
|
|
151
|
-
beforeEach(() => {
|
|
152
|
-
vi.restoreAllMocks();
|
|
153
|
-
// resolveEntity delegates to resolveEntityContext (context.ts), which
|
|
154
|
-
// caches by UID. The cache must be cleared between tests, otherwise a
|
|
155
|
-
// 200 response from the success test sticks around and the subsequent
|
|
156
|
-
// 403/500 tests see a cached EntityContext and never call fetch.
|
|
157
|
-
clearContextCache();
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
afterEach(() => {
|
|
161
|
-
vi.restoreAllMocks();
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
describe("resolveEntity", () => {
|
|
165
|
-
it("resolves entity by slug (happy path)", async () => {
|
|
166
|
-
setupFetchMock();
|
|
167
|
-
|
|
168
|
-
const ctx = await resolveEntity({ slug: "indigo", vaultConfig: VAULT_CONFIG });
|
|
169
|
-
|
|
170
|
-
expect(ctx.uid).toBe("cmp_indigo_001");
|
|
171
|
-
expect(ctx.slug).toBe("indigo");
|
|
172
|
-
expect(ctx.bucketName).toBe("hq-vault-indigo");
|
|
173
|
-
expect(ctx.region).toBe("us-east-1");
|
|
174
|
-
expect(ctx.credentials?.accessKeyId).toBe("ASIAMOCK000000000001");
|
|
175
|
-
expect(ctx.credentials?.secretAccessKey).toBe("mockSecret");
|
|
176
|
-
expect(ctx.credentials?.sessionToken).toBe("mockSession");
|
|
177
|
-
expect(ctx.expiresAt).toBeTruthy();
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("resolves 'personal' slug without special-casing", async () => {
|
|
181
|
-
setupFetchMock();
|
|
182
|
-
|
|
183
|
-
const ctx = await resolveEntity({ slug: "personal", vaultConfig: VAULT_CONFIG });
|
|
184
|
-
|
|
185
|
-
expect(ctx.uid).toBe("cmp_personal_001");
|
|
186
|
-
expect(ctx.slug).toBe("personal");
|
|
187
|
-
expect(ctx.bucketName).toBe("hq-vault-personal");
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("throws EntityNotFoundError when slug not found", async () => {
|
|
191
|
-
setupFetchMock();
|
|
192
|
-
|
|
193
|
-
await expect(
|
|
194
|
-
resolveEntity({ slug: "unknown", vaultConfig: VAULT_CONFIG }),
|
|
195
|
-
).rejects.toThrow(EntityNotFoundError);
|
|
196
|
-
|
|
197
|
-
try {
|
|
198
|
-
await resolveEntity({ slug: "unknown", vaultConfig: VAULT_CONFIG });
|
|
199
|
-
} catch (err) {
|
|
200
|
-
expect(err).toBeInstanceOf(EntityNotFoundError);
|
|
201
|
-
const e = err as EntityNotFoundError;
|
|
202
|
-
expect(e.message).toContain("unknown");
|
|
203
|
-
expect(e.message).toContain("indigo");
|
|
204
|
-
expect(e.message).toContain("personal");
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it("throws EntityNotFoundError with (none) when user has no memberships", async () => {
|
|
209
|
-
setupFetchMock({ memberships: [] });
|
|
210
|
-
|
|
211
|
-
await expect(
|
|
212
|
-
resolveEntity({ slug: "indigo", vaultConfig: VAULT_CONFIG }),
|
|
213
|
-
).rejects.toThrow(EntityNotFoundError);
|
|
214
|
-
|
|
215
|
-
try {
|
|
216
|
-
await resolveEntity({ slug: "indigo", vaultConfig: VAULT_CONFIG });
|
|
217
|
-
} catch (err) {
|
|
218
|
-
expect((err as Error).message).toContain("(none)");
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it("throws EntityPermissionError on 403 from STS", async () => {
|
|
223
|
-
setupFetchMock({ stsStatus: 403, stsBody: { error: "Permission denied" } });
|
|
224
|
-
|
|
225
|
-
await expect(
|
|
226
|
-
resolveEntity({ slug: "indigo", vaultConfig: VAULT_CONFIG }),
|
|
227
|
-
).rejects.toThrow(EntityPermissionError);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it("throws EntityResolutionError on 500 from STS", async () => {
|
|
231
|
-
setupFetchMock({ stsStatus: 500, stsBody: { error: "Internal error" } });
|
|
232
|
-
|
|
233
|
-
let caught: unknown;
|
|
234
|
-
try {
|
|
235
|
-
await resolveEntity({ slug: "indigo", vaultConfig: VAULT_CONFIG });
|
|
236
|
-
} catch (err) {
|
|
237
|
-
caught = err;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
expect(caught).toBeInstanceOf(EntityResolutionError);
|
|
241
|
-
expect((caught as EntityResolutionError).statusCode).toBe(500);
|
|
242
|
-
}, 15_000);
|
|
243
|
-
|
|
244
|
-
it("rejects slug case-mismatch (case-sensitive)", async () => {
|
|
245
|
-
setupFetchMock();
|
|
246
|
-
|
|
247
|
-
await expect(
|
|
248
|
-
resolveEntity({ slug: "Indigo", vaultConfig: VAULT_CONFIG }),
|
|
249
|
-
).rejects.toThrow(EntityNotFoundError);
|
|
250
|
-
|
|
251
|
-
try {
|
|
252
|
-
await resolveEntity({ slug: "Indigo", vaultConfig: VAULT_CONFIG });
|
|
253
|
-
} catch (err) {
|
|
254
|
-
expect((err as Error).message).toContain("Indigo");
|
|
255
|
-
expect((err as Error).message).toContain("indigo");
|
|
256
|
-
}
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it("throws EntityResolutionError when entity has no bucket", async () => {
|
|
260
|
-
setupFetchMock({
|
|
261
|
-
entities: {
|
|
262
|
-
cmp_indigo_001: { ...ENTITIES.indigo, bucketName: undefined as unknown as string },
|
|
263
|
-
cmp_personal_001: ENTITIES.personal,
|
|
264
|
-
},
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
await expect(
|
|
268
|
-
resolveEntity({ slug: "indigo", vaultConfig: VAULT_CONFIG }),
|
|
269
|
-
).rejects.toThrow(EntityResolutionError);
|
|
270
|
-
|
|
271
|
-
try {
|
|
272
|
-
await resolveEntity({ slug: "indigo", vaultConfig: VAULT_CONFIG });
|
|
273
|
-
} catch (err) {
|
|
274
|
-
expect((err as Error).message).toContain("no bucket provisioned");
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
describe("listAvailableEntities", () => {
|
|
280
|
-
it("returns all available entities with slug, uid, and role", async () => {
|
|
281
|
-
setupFetchMock();
|
|
282
|
-
|
|
283
|
-
const entities = await listAvailableEntities({ vaultConfig: VAULT_CONFIG });
|
|
284
|
-
|
|
285
|
-
expect(entities).toHaveLength(2);
|
|
286
|
-
expect(entities).toEqual(
|
|
287
|
-
expect.arrayContaining([
|
|
288
|
-
{ slug: "indigo", uid: "cmp_indigo_001", role: "owner" },
|
|
289
|
-
{ slug: "personal", uid: "cmp_personal_001", role: "owner" },
|
|
290
|
-
]),
|
|
291
|
-
);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
it("returns empty array when user has no memberships", async () => {
|
|
295
|
-
setupFetchMock({ memberships: [] });
|
|
296
|
-
|
|
297
|
-
const entities = await listAvailableEntities({ vaultConfig: VAULT_CONFIG });
|
|
298
|
-
|
|
299
|
-
expect(entities).toEqual([]);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it("preserves role from membership", async () => {
|
|
303
|
-
setupFetchMock({
|
|
304
|
-
memberships: [
|
|
305
|
-
{ companyUid: "cmp_indigo_001", role: "member" },
|
|
306
|
-
],
|
|
307
|
-
entities: { cmp_indigo_001: ENTITIES.indigo },
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
const entities = await listAvailableEntities({ vaultConfig: VAULT_CONFIG });
|
|
311
|
-
|
|
312
|
-
expect(entities).toHaveLength(1);
|
|
313
|
-
expect(entities[0].role).toBe("member");
|
|
314
|
-
});
|
|
315
|
-
});
|
package/src/entity-resolver.ts
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Entity resolver — maps a human-readable slug to an EntityContext with
|
|
3
|
-
* STS-vended credentials.
|
|
4
|
-
*
|
|
5
|
-
* Uses the membership-based discovery path (GET /membership/me → entity lookup)
|
|
6
|
-
* rather than JWT claims, which are fragile post-migration (see
|
|
7
|
-
* indigo-jwt-claims-fragile-post-migration). Delegates STS vending to the
|
|
8
|
-
* canonical `resolveEntityContext()` in context.ts so we use the actual
|
|
9
|
-
* vault-service endpoint (POST /sts/vend) rather than a made-up one.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
VaultClient,
|
|
14
|
-
VaultClientError,
|
|
15
|
-
VaultPermissionDeniedError,
|
|
16
|
-
} from "./vault-client.js";
|
|
17
|
-
import type { MembershipRole } from "./vault-client.js";
|
|
18
|
-
import type { EntityContext, VaultServiceConfig } from "./types.js";
|
|
19
|
-
import { resolveEntityContext } from "./context.js";
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// Error classes
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
export class EntityNotFoundError extends Error {
|
|
26
|
-
constructor(slug: string, availableSlugs: string[]) {
|
|
27
|
-
const available = availableSlugs.length > 0
|
|
28
|
-
? availableSlugs.join(", ")
|
|
29
|
-
: "(none)";
|
|
30
|
-
super(`Entity '${slug}' not found. Available: ${available}`);
|
|
31
|
-
this.name = "EntityNotFoundError";
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export class EntityPermissionError extends Error {
|
|
36
|
-
constructor(slug: string, message?: string) {
|
|
37
|
-
super(message ?? `Permission denied for entity '${slug}'`);
|
|
38
|
-
this.name = "EntityPermissionError";
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export class EntityResolutionError extends Error {
|
|
43
|
-
constructor(
|
|
44
|
-
message: string,
|
|
45
|
-
public readonly statusCode: number,
|
|
46
|
-
public readonly body?: string,
|
|
47
|
-
) {
|
|
48
|
-
super(message);
|
|
49
|
-
this.name = "EntityResolutionError";
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
// Public types
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
|
|
57
|
-
export interface AvailableEntity {
|
|
58
|
-
slug: string;
|
|
59
|
-
uid: string;
|
|
60
|
-
role: MembershipRole;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
// resolveEntity
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Resolve a human-readable entity slug to a fully hydrated EntityContext
|
|
69
|
-
* with STS-scoped credentials ready for S3 operations.
|
|
70
|
-
*
|
|
71
|
-
* Flow:
|
|
72
|
-
* 1. GET /membership/me → list user's active memberships (companyUids)
|
|
73
|
-
* 2. GET /entity/{uid} for each → resolve slug + bucketName
|
|
74
|
-
* 3. Match the requested slug (case-sensitive) — throw EntityNotFoundError
|
|
75
|
-
* with the list of available slugs if it doesn't match (better UX than
|
|
76
|
-
* the raw 404 from /entity/by-slug/{slug}).
|
|
77
|
-
* 4. Delegate to resolveEntityContext(uid) → POST /sts/vend (the actual
|
|
78
|
-
* vault-service endpoint).
|
|
79
|
-
*/
|
|
80
|
-
export async function resolveEntity(opts: {
|
|
81
|
-
slug: string;
|
|
82
|
-
vaultConfig: VaultServiceConfig;
|
|
83
|
-
}): Promise<EntityContext> {
|
|
84
|
-
const client = new VaultClient(opts.vaultConfig);
|
|
85
|
-
|
|
86
|
-
// Step 1: List memberships
|
|
87
|
-
const memberships = await client.listMyMemberships();
|
|
88
|
-
|
|
89
|
-
if (memberships.length === 0) {
|
|
90
|
-
throw new EntityNotFoundError(opts.slug, []);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Step 2: Resolve entity info for each membership (parallel)
|
|
94
|
-
const resolved = await Promise.all(
|
|
95
|
-
memberships.map(async (m) => {
|
|
96
|
-
const entity = await client.entity.get(m.companyUid);
|
|
97
|
-
return { entity, role: m.role };
|
|
98
|
-
}),
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
// Step 3: Find matching slug (case-sensitive)
|
|
102
|
-
const match = resolved.find(({ entity }) => entity.slug === opts.slug);
|
|
103
|
-
|
|
104
|
-
if (!match) {
|
|
105
|
-
const availableSlugs = resolved.map(({ entity }) => entity.slug);
|
|
106
|
-
throw new EntityNotFoundError(opts.slug, availableSlugs);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (!match.entity.bucketName) {
|
|
110
|
-
throw new EntityResolutionError(
|
|
111
|
-
`Entity '${opts.slug}' (${match.entity.uid}) has no bucket provisioned`,
|
|
112
|
-
500,
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Step 4: Vend STS credentials via the canonical context resolver.
|
|
117
|
-
// resolveEntityContext re-fetches the entity by UID (one extra hop) and
|
|
118
|
-
// calls POST /sts/vend (the actual endpoint) — re-using its caching +
|
|
119
|
-
// refresh logic is worth more than the redundant fetch.
|
|
120
|
-
try {
|
|
121
|
-
return await resolveEntityContext(match.entity.uid, opts.vaultConfig);
|
|
122
|
-
} catch (err) {
|
|
123
|
-
if (err instanceof VaultPermissionDeniedError) {
|
|
124
|
-
throw new EntityPermissionError(opts.slug, err.message);
|
|
125
|
-
}
|
|
126
|
-
if (err instanceof VaultClientError) {
|
|
127
|
-
throw new EntityResolutionError(
|
|
128
|
-
`STS vending failed for entity '${opts.slug}': ${err.message}`,
|
|
129
|
-
err.statusCode,
|
|
130
|
-
err.body,
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
// resolveEntityContext throws plain Error on STS HTTP failures
|
|
134
|
-
// (`STS /sts/vend failed: <status> <body>`). Extract the status code
|
|
135
|
-
// and surface the right typed error for the caller.
|
|
136
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
137
|
-
const statusMatch = /STS\s+\S+\s+failed:\s+(\d{3})\b/i.exec(msg);
|
|
138
|
-
const statusCode = statusMatch ? parseInt(statusMatch[1], 10) : 500;
|
|
139
|
-
if (statusCode === 403) {
|
|
140
|
-
throw new EntityPermissionError(opts.slug, msg);
|
|
141
|
-
}
|
|
142
|
-
throw new EntityResolutionError(
|
|
143
|
-
`STS vending failed for entity '${opts.slug}': ${msg}`,
|
|
144
|
-
statusCode,
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// ---------------------------------------------------------------------------
|
|
150
|
-
// listAvailableEntities
|
|
151
|
-
// ---------------------------------------------------------------------------
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* List all entities the caller has access to, with slug, UID, and role.
|
|
155
|
-
* Used by `hq sources entities` / `hq signals entities` for discovery.
|
|
156
|
-
*/
|
|
157
|
-
export async function listAvailableEntities(opts: {
|
|
158
|
-
vaultConfig: VaultServiceConfig;
|
|
159
|
-
}): Promise<AvailableEntity[]> {
|
|
160
|
-
const client = new VaultClient(opts.vaultConfig);
|
|
161
|
-
|
|
162
|
-
const memberships = await client.listMyMemberships();
|
|
163
|
-
|
|
164
|
-
if (memberships.length === 0) {
|
|
165
|
-
return [];
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const resolved = await Promise.all(
|
|
169
|
-
memberships.map(async (m) => {
|
|
170
|
-
const entity = await client.entity.get(m.companyUid);
|
|
171
|
-
return {
|
|
172
|
-
slug: entity.slug,
|
|
173
|
-
uid: entity.uid,
|
|
174
|
-
role: m.role,
|
|
175
|
-
};
|
|
176
|
-
}),
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
return resolved;
|
|
180
|
-
}
|