@indigoai-us/hq-cloud 5.1.0 → 5.1.9

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 (100) hide show
  1. package/dist/bin/sync-runner.d.ts +134 -0
  2. package/dist/bin/sync-runner.d.ts.map +1 -0
  3. package/dist/bin/sync-runner.js +360 -0
  4. package/dist/bin/sync-runner.js.map +1 -0
  5. package/dist/bin/sync-runner.test.d.ts +10 -0
  6. package/dist/bin/sync-runner.test.d.ts.map +1 -0
  7. package/dist/bin/sync-runner.test.js +648 -0
  8. package/dist/bin/sync-runner.test.js.map +1 -0
  9. package/dist/cli/index.d.ts +1 -1
  10. package/dist/cli/index.d.ts.map +1 -1
  11. package/dist/cli/share.js +2 -2
  12. package/dist/cli/share.js.map +1 -1
  13. package/dist/cli/share.test.js +9 -1
  14. package/dist/cli/share.test.js.map +1 -1
  15. package/dist/cli/sync.d.ts +28 -0
  16. package/dist/cli/sync.d.ts.map +1 -1
  17. package/dist/cli/sync.js +33 -10
  18. package/dist/cli/sync.js.map +1 -1
  19. package/dist/cli/sync.test.js +15 -4
  20. package/dist/cli/sync.test.js.map +1 -1
  21. package/dist/cognito-auth.d.ts.map +1 -1
  22. package/dist/cognito-auth.js +19 -1
  23. package/dist/cognito-auth.js.map +1 -1
  24. package/dist/cognito-auth.test.d.ts +9 -0
  25. package/dist/cognito-auth.test.d.ts.map +1 -0
  26. package/dist/cognito-auth.test.js +113 -0
  27. package/dist/cognito-auth.test.js.map +1 -0
  28. package/dist/context.d.ts.map +1 -1
  29. package/dist/context.js +1 -0
  30. package/dist/context.js.map +1 -1
  31. package/dist/daemon-worker.d.ts +6 -1
  32. package/dist/daemon-worker.d.ts.map +1 -1
  33. package/dist/daemon-worker.js +12 -16
  34. package/dist/daemon-worker.js.map +1 -1
  35. package/dist/daemon.d.ts +2 -0
  36. package/dist/daemon.d.ts.map +1 -1
  37. package/dist/daemon.js +2 -0
  38. package/dist/daemon.js.map +1 -1
  39. package/dist/ignore.d.ts +13 -2
  40. package/dist/ignore.d.ts.map +1 -1
  41. package/dist/ignore.js +69 -12
  42. package/dist/ignore.js.map +1 -1
  43. package/dist/index.d.ts +24 -28
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +19 -134
  46. package/dist/index.js.map +1 -1
  47. package/dist/journal.d.ts +20 -4
  48. package/dist/journal.d.ts.map +1 -1
  49. package/dist/journal.js +45 -8
  50. package/dist/journal.js.map +1 -1
  51. package/dist/journal.test.d.ts +9 -0
  52. package/dist/journal.test.d.ts.map +1 -0
  53. package/dist/journal.test.js +114 -0
  54. package/dist/journal.test.js.map +1 -0
  55. package/dist/s3.d.ts +18 -6
  56. package/dist/s3.d.ts.map +1 -1
  57. package/dist/s3.js +57 -56
  58. package/dist/s3.js.map +1 -1
  59. package/dist/types.d.ts +34 -0
  60. package/dist/types.d.ts.map +1 -1
  61. package/dist/vault-client.d.ts +59 -0
  62. package/dist/vault-client.d.ts.map +1 -1
  63. package/dist/vault-client.js +72 -0
  64. package/dist/vault-client.js.map +1 -1
  65. package/dist/vault-client.test.js +160 -0
  66. package/dist/vault-client.test.js.map +1 -1
  67. package/dist/watcher.d.ts +7 -1
  68. package/dist/watcher.d.ts.map +1 -1
  69. package/dist/watcher.js +11 -5
  70. package/dist/watcher.js.map +1 -1
  71. package/package.json +15 -3
  72. package/src/bin/sync-runner.test.ts +804 -0
  73. package/src/bin/sync-runner.ts +499 -0
  74. package/src/cli/accept.ts +97 -0
  75. package/src/cli/conflict.ts +119 -0
  76. package/src/cli/index.ts +25 -0
  77. package/src/cli/invite.test.ts +247 -0
  78. package/src/cli/invite.ts +180 -0
  79. package/src/cli/promote.ts +123 -0
  80. package/src/cli/share.test.ts +155 -0
  81. package/src/cli/share.ts +212 -0
  82. package/src/cli/sync.test.ts +225 -0
  83. package/src/cli/sync.ts +225 -0
  84. package/src/cognito-auth.test.ts +156 -0
  85. package/src/cognito-auth.ts +18 -1
  86. package/src/context.test.ts +202 -0
  87. package/src/context.ts +178 -0
  88. package/src/daemon-worker.ts +13 -19
  89. package/src/daemon.ts +2 -0
  90. package/src/ignore.ts +76 -12
  91. package/src/index.ts +94 -165
  92. package/src/journal.test.ts +146 -0
  93. package/src/journal.ts +53 -11
  94. package/src/s3.ts +76 -66
  95. package/src/types.ts +37 -0
  96. package/src/vault-client.test.ts +563 -0
  97. package/src/vault-client.ts +478 -0
  98. package/src/watcher.ts +12 -5
  99. package/test/invite-flow.integration.test.ts +244 -0
  100. package/test/share-sync.integration.test.ts +210 -0
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Unit tests for the sync journal (ADR-0001 Phase 5).
3
+ *
4
+ * Verifies per-company isolation, HQ_STATE_DIR override, and filename
5
+ * sanitization — all behaviors that the pre-Phase-5 monolithic journal
6
+ * didn't need.
7
+ */
8
+
9
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
10
+ import * as fs from "fs";
11
+ import * as os from "os";
12
+ import * as path from "path";
13
+ import {
14
+ getJournalPath,
15
+ getStateDir,
16
+ readJournal,
17
+ writeJournal,
18
+ updateEntry,
19
+ } from "./journal.js";
20
+ import type { SyncJournal } from "./types.js";
21
+
22
+ describe("journal", () => {
23
+ let stateDir: string;
24
+
25
+ beforeEach(() => {
26
+ stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-journal-test-"));
27
+ process.env.HQ_STATE_DIR = stateDir;
28
+ });
29
+
30
+ afterEach(() => {
31
+ fs.rmSync(stateDir, { recursive: true, force: true });
32
+ delete process.env.HQ_STATE_DIR;
33
+ });
34
+
35
+ describe("getStateDir", () => {
36
+ it("honors HQ_STATE_DIR env var", () => {
37
+ expect(getStateDir()).toBe(stateDir);
38
+ });
39
+
40
+ it("falls back to ~/.hq when env var unset", () => {
41
+ delete process.env.HQ_STATE_DIR;
42
+ expect(getStateDir()).toBe(path.join(os.homedir(), ".hq"));
43
+ });
44
+ });
45
+
46
+ describe("getJournalPath", () => {
47
+ it("produces a per-slug filename", () => {
48
+ expect(getJournalPath("indigo")).toBe(
49
+ path.join(stateDir, "sync-journal.indigo.json"),
50
+ );
51
+ });
52
+
53
+ it("isolates different slugs into different files", () => {
54
+ expect(getJournalPath("indigo")).not.toBe(getJournalPath("brandstage"));
55
+ });
56
+
57
+ it("sanitizes path-unsafe characters", () => {
58
+ expect(getJournalPath("foo/bar")).toBe(
59
+ path.join(stateDir, "sync-journal.foo_bar.json"),
60
+ );
61
+ expect(getJournalPath("../escape")).toBe(
62
+ path.join(stateDir, "sync-journal.___escape.json"),
63
+ );
64
+ });
65
+
66
+ it("throws on empty slug", () => {
67
+ expect(() => getJournalPath("")).toThrow(/slug is required/);
68
+ });
69
+
70
+ it("throws on slug that sanitizes to empty", () => {
71
+ expect(() => getJournalPath("///")).toThrow(/empty identifier/);
72
+ });
73
+ });
74
+
75
+ describe("readJournal", () => {
76
+ it("returns an empty journal when the file doesn't exist", () => {
77
+ const j = readJournal("indigo");
78
+ expect(j.version).toBe("1");
79
+ expect(j.files).toEqual({});
80
+ expect(j.lastSync).toBe("");
81
+ });
82
+
83
+ it("reads a journal written with writeJournal", () => {
84
+ const original: SyncJournal = {
85
+ version: "1",
86
+ lastSync: "2026-04-19T00:00:00.000Z",
87
+ files: {
88
+ "docs/handoff.md": {
89
+ hash: "abc123",
90
+ size: 42,
91
+ syncedAt: "2026-04-19T00:00:00.000Z",
92
+ direction: "down",
93
+ },
94
+ },
95
+ };
96
+ writeJournal("indigo", original);
97
+ const roundTripped = readJournal("indigo");
98
+ expect(roundTripped).toEqual(original);
99
+ });
100
+ });
101
+
102
+ describe("writeJournal", () => {
103
+ it("creates the state directory if it doesn't exist", () => {
104
+ const nestedDir = path.join(stateDir, "nested", "deep");
105
+ process.env.HQ_STATE_DIR = nestedDir;
106
+ expect(fs.existsSync(nestedDir)).toBe(false);
107
+
108
+ writeJournal("indigo", { version: "1", lastSync: "", files: {} });
109
+ expect(fs.existsSync(nestedDir)).toBe(true);
110
+ expect(
111
+ fs.existsSync(path.join(nestedDir, "sync-journal.indigo.json")),
112
+ ).toBe(true);
113
+ });
114
+
115
+ it("keeps per-company journals independent", () => {
116
+ writeJournal("indigo", {
117
+ version: "1",
118
+ lastSync: "",
119
+ files: { "a.md": { hash: "1", size: 1, syncedAt: "", direction: "up" } },
120
+ });
121
+ writeJournal("brandstage", {
122
+ version: "1",
123
+ lastSync: "",
124
+ files: { "b.md": { hash: "2", size: 2, syncedAt: "", direction: "up" } },
125
+ });
126
+
127
+ const indigo = readJournal("indigo");
128
+ const brandstage = readJournal("brandstage");
129
+ expect(indigo.files).toHaveProperty("a.md");
130
+ expect(indigo.files).not.toHaveProperty("b.md");
131
+ expect(brandstage.files).toHaveProperty("b.md");
132
+ expect(brandstage.files).not.toHaveProperty("a.md");
133
+ });
134
+ });
135
+
136
+ describe("updateEntry", () => {
137
+ it("stamps lastSync and the per-file syncedAt", () => {
138
+ const j: SyncJournal = { version: "1", lastSync: "", files: {} };
139
+ updateEntry(j, "foo.md", "hash", 10, "up");
140
+ expect(j.files["foo.md"]?.hash).toBe("hash");
141
+ expect(j.files["foo.md"]?.direction).toBe("up");
142
+ expect(j.lastSync).not.toBe("");
143
+ expect(j.files["foo.md"]?.syncedAt).not.toBe("");
144
+ });
145
+ });
146
+ });
package/src/journal.ts CHANGED
@@ -1,20 +1,61 @@
1
1
  /**
2
- * Sync journal — tracks file state for conflict detection
2
+ * Sync journal — tracks per-file state (hash, size, last-synced direction) so
3
+ * sync/share can detect local edits that would be clobbered by a blind pull.
4
+ *
5
+ * ADR-0001 Phase 5: the journal is sharded by company slug and lives in
6
+ * `~/.hq/`, not inside the HQ content root. One monolithic journal per HQ
7
+ * install conflates state across companies and forces every runner to
8
+ * serialize through the same file — splitting it lets `hq-sync-runner
9
+ * --companies` fan out without contention, and a corrupted shard only affects
10
+ * one company.
11
+ *
12
+ * Path: `{stateDir}/sync-journal.{slug}.json`, where `stateDir` resolves to
13
+ * `HQ_STATE_DIR` (if set) or `~/.hq`.
3
14
  */
4
15
 
5
16
  import * as fs from "fs";
17
+ import * as os from "os";
6
18
  import * as path from "path";
7
19
  import * as crypto from "crypto";
8
20
  import type { SyncJournal, JournalEntry } from "./types.js";
9
21
 
10
- const JOURNAL_FILE = ".hq-sync-journal.json";
22
+ const JOURNAL_FILE_PREFIX = "sync-journal.";
23
+ const JOURNAL_FILE_SUFFIX = ".json";
11
24
 
12
- export function getJournalPath(hqRoot: string): string {
13
- return path.join(hqRoot, JOURNAL_FILE);
25
+ /**
26
+ * Where per-company journals are stored. Honors `HQ_STATE_DIR` for tests and
27
+ * non-standard installs; otherwise falls back to `~/.hq`.
28
+ */
29
+ export function getStateDir(): string {
30
+ return process.env.HQ_STATE_DIR ?? path.join(os.homedir(), ".hq");
31
+ }
32
+
33
+ /**
34
+ * Filename-safe form of a slug. Slugs from vault-service are already
35
+ * URL-safe, but this guards against paths, dots, or anything the filesystem
36
+ * might interpret. Empty-or-invalid slugs throw rather than silently writing
37
+ * to a shared "sync-journal..json" file.
38
+ */
39
+ function sanitizeSlug(slug: string): string {
40
+ if (!slug) {
41
+ throw new Error("journal: slug is required (empty or undefined)");
42
+ }
43
+ const cleaned = slug.replace(/[^a-zA-Z0-9_-]/g, "_");
44
+ if (!cleaned || /^[_-]+$/.test(cleaned)) {
45
+ throw new Error(`journal: slug "${slug}" sanitizes to an empty identifier`);
46
+ }
47
+ return cleaned;
48
+ }
49
+
50
+ export function getJournalPath(slug: string): string {
51
+ return path.join(
52
+ getStateDir(),
53
+ `${JOURNAL_FILE_PREFIX}${sanitizeSlug(slug)}${JOURNAL_FILE_SUFFIX}`,
54
+ );
14
55
  }
15
56
 
16
- export function readJournal(hqRoot: string): SyncJournal {
17
- const journalPath = getJournalPath(hqRoot);
57
+ export function readJournal(slug: string): SyncJournal {
58
+ const journalPath = getJournalPath(slug);
18
59
  if (fs.existsSync(journalPath)) {
19
60
  const content = fs.readFileSync(journalPath, "utf-8");
20
61
  return JSON.parse(content) as SyncJournal;
@@ -22,8 +63,9 @@ export function readJournal(hqRoot: string): SyncJournal {
22
63
  return { version: "1", lastSync: "", files: {} };
23
64
  }
24
65
 
25
- export function writeJournal(hqRoot: string, journal: SyncJournal): void {
26
- const journalPath = getJournalPath(hqRoot);
66
+ export function writeJournal(slug: string, journal: SyncJournal): void {
67
+ const journalPath = getJournalPath(slug);
68
+ fs.mkdirSync(path.dirname(journalPath), { recursive: true });
27
69
  fs.writeFileSync(journalPath, JSON.stringify(journal, null, 2));
28
70
  }
29
71
 
@@ -37,7 +79,7 @@ export function updateEntry(
37
79
  relativePath: string,
38
80
  hash: string,
39
81
  size: number,
40
- direction: "up" | "down"
82
+ direction: "up" | "down",
41
83
  ): void {
42
84
  journal.files[relativePath] = {
43
85
  hash,
@@ -50,14 +92,14 @@ export function updateEntry(
50
92
 
51
93
  export function getEntry(
52
94
  journal: SyncJournal,
53
- relativePath: string
95
+ relativePath: string,
54
96
  ): JournalEntry | undefined {
55
97
  return journal.files[relativePath];
56
98
  }
57
99
 
58
100
  export function removeEntry(
59
101
  journal: SyncJournal,
60
- relativePath: string
102
+ relativePath: string,
61
103
  ): void {
62
104
  delete journal.files[relativePath];
63
105
  }
package/src/s3.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  /**
2
- * S3 operations — upload, download, list, delete
2
+ * S3 operations — upload, download, list, delete.
3
+ *
4
+ * VLT-5: All operations now accept an EntityContext (entity-aware bucket +
5
+ * STS-scoped credentials) instead of reading static env config. The caller
6
+ * is responsible for resolving the context via resolveEntityContext().
3
7
  */
4
8
 
5
9
  import * as fs from "fs";
@@ -10,79 +14,56 @@ import {
10
14
  GetObjectCommand,
11
15
  ListObjectsV2Command,
12
16
  DeleteObjectCommand,
17
+ HeadObjectCommand,
13
18
  } from "@aws-sdk/client-s3";
14
- import type { Credentials, SyncConfig } from "./types.js";
15
- import { readCredentials, refreshAwsCredentials } from "./auth.js";
16
-
17
- let s3Client: S3Client | null = null;
18
-
19
- function getConfig(creds: Credentials): SyncConfig {
20
- const prefix = creds.teamId
21
- ? `teams/${creds.teamId}/users/${creds.userId}/hq/`
22
- : `users/${creds.userId}/hq/`;
23
- return {
24
- bucket: creds.bucket,
25
- region: creds.region,
26
- userId: creds.userId,
27
- prefix,
28
- };
29
- }
30
-
31
- async function getClient(): Promise<{ client: S3Client; config: SyncConfig }> {
32
- let creds = readCredentials();
33
- if (!creds) {
34
- throw new Error("Not authenticated. Run 'hq sync init' first.");
35
- }
36
-
37
- // Refresh if expired or missing access key
38
- if (!creds.accessKeyId || (creds.expiration && new Date(creds.expiration) < new Date())) {
39
- creds = await refreshAwsCredentials(creds);
40
- }
41
-
42
- if (!s3Client) {
43
- s3Client = new S3Client({
44
- region: creds.region,
45
- credentials: {
46
- accessKeyId: creds.accessKeyId,
47
- secretAccessKey: creds.secretAccessKey,
48
- sessionToken: creds.sessionToken,
49
- },
50
- });
51
- }
19
+ import type { EntityContext } from "./types.js";
52
20
 
53
- return { client: s3Client, config: getConfig(creds) };
21
+ /**
22
+ * Build an S3Client from an EntityContext's STS-scoped credentials.
23
+ * A new client is created each time to ensure fresh credentials are used
24
+ * (the caller handles caching/refresh at the EntityContext level).
25
+ */
26
+ function buildClient(ctx: EntityContext): S3Client {
27
+ return new S3Client({
28
+ region: ctx.region,
29
+ credentials: {
30
+ accessKeyId: ctx.credentials.accessKeyId,
31
+ secretAccessKey: ctx.credentials.secretAccessKey,
32
+ sessionToken: ctx.credentials.sessionToken,
33
+ },
34
+ });
54
35
  }
55
36
 
56
37
  export async function uploadFile(
38
+ ctx: EntityContext,
57
39
  localPath: string,
58
- relativePath: string
40
+ key: string,
59
41
  ): Promise<void> {
60
- const { client, config } = await getClient();
61
- const key = `${config.prefix}${relativePath}`;
42
+ const client = buildClient(ctx);
62
43
  const body = fs.readFileSync(localPath);
63
44
 
64
45
  await client.send(
65
46
  new PutObjectCommand({
66
- Bucket: config.bucket,
47
+ Bucket: ctx.bucketName,
67
48
  Key: key,
68
49
  Body: body,
69
- ContentType: getMimeType(relativePath),
70
- })
50
+ ContentType: getMimeType(key),
51
+ }),
71
52
  );
72
53
  }
73
54
 
74
55
  export async function downloadFile(
75
- relativePath: string,
76
- localPath: string
56
+ ctx: EntityContext,
57
+ key: string,
58
+ localPath: string,
77
59
  ): Promise<void> {
78
- const { client, config } = await getClient();
79
- const key = `${config.prefix}${relativePath}`;
60
+ const client = buildClient(ctx);
80
61
 
81
62
  const response = await client.send(
82
63
  new GetObjectCommand({
83
- Bucket: config.bucket,
64
+ Bucket: ctx.bucketName,
84
65
  Key: key,
85
- })
66
+ }),
86
67
  );
87
68
 
88
69
  if (!response.Body) {
@@ -104,34 +85,33 @@ export async function downloadFile(
104
85
 
105
86
  export interface RemoteFile {
106
87
  key: string;
107
- relativePath: string;
108
88
  size: number;
109
89
  lastModified: Date;
110
90
  etag: string;
111
91
  }
112
92
 
113
- export async function listRemoteFiles(): Promise<RemoteFile[]> {
114
- const { client, config } = await getClient();
93
+ export async function listRemoteFiles(
94
+ ctx: EntityContext,
95
+ prefix?: string,
96
+ ): Promise<RemoteFile[]> {
97
+ const client = buildClient(ctx);
115
98
  const files: RemoteFile[] = [];
116
99
  let continuationToken: string | undefined;
117
100
 
118
101
  do {
119
102
  const response = await client.send(
120
103
  new ListObjectsV2Command({
121
- Bucket: config.bucket,
122
- Prefix: config.prefix,
104
+ Bucket: ctx.bucketName,
105
+ Prefix: prefix,
123
106
  ContinuationToken: continuationToken,
124
- })
107
+ }),
125
108
  );
126
109
 
127
110
  for (const obj of response.Contents || []) {
128
111
  if (!obj.Key || !obj.Size) continue;
129
- const relativePath = obj.Key.replace(config.prefix, "");
130
- if (!relativePath) continue;
131
112
 
132
113
  files.push({
133
114
  key: obj.Key,
134
- relativePath,
135
115
  size: obj.Size,
136
116
  lastModified: obj.LastModified || new Date(),
137
117
  etag: obj.ETag || "",
@@ -144,18 +124,48 @@ export async function listRemoteFiles(): Promise<RemoteFile[]> {
144
124
  return files;
145
125
  }
146
126
 
147
- export async function deleteRemoteFile(relativePath: string): Promise<void> {
148
- const { client, config } = await getClient();
149
- const key = `${config.prefix}${relativePath}`;
127
+ export async function deleteRemoteFile(
128
+ ctx: EntityContext,
129
+ key: string,
130
+ ): Promise<void> {
131
+ const client = buildClient(ctx);
150
132
 
151
133
  await client.send(
152
134
  new DeleteObjectCommand({
153
- Bucket: config.bucket,
135
+ Bucket: ctx.bucketName,
154
136
  Key: key,
155
- })
137
+ }),
156
138
  );
157
139
  }
158
140
 
141
+ /**
142
+ * Check if a remote key exists and return its metadata.
143
+ */
144
+ export async function headRemoteFile(
145
+ ctx: EntityContext,
146
+ key: string,
147
+ ): Promise<{ lastModified: Date; etag: string; size: number } | null> {
148
+ const client = buildClient(ctx);
149
+ try {
150
+ const response = await client.send(
151
+ new HeadObjectCommand({
152
+ Bucket: ctx.bucketName,
153
+ Key: key,
154
+ }),
155
+ );
156
+ return {
157
+ lastModified: response.LastModified || new Date(),
158
+ etag: response.ETag || "",
159
+ size: response.ContentLength || 0,
160
+ };
161
+ } catch (err: unknown) {
162
+ if (err && typeof err === "object" && "name" in err && err.name === "NotFound") {
163
+ return null;
164
+ }
165
+ throw err;
166
+ }
167
+ }
168
+
159
169
  function getMimeType(filePath: string): string {
160
170
  const ext = path.extname(filePath).toLowerCase();
161
171
  const mimeTypes: Record<string, string> = {
package/src/types.ts CHANGED
@@ -57,3 +57,40 @@ export interface DaemonState {
57
57
  startedAt: string;
58
58
  hqRoot: string;
59
59
  }
60
+
61
+ /**
62
+ * Entity-aware context for vault-backed S3 operations (VLT-5).
63
+ * Resolved from vault-service entity registry + STS vending.
64
+ */
65
+ export interface EntityContext {
66
+ /** Entity UID (cmp_*) */
67
+ uid: string;
68
+ /** Entity slug (human-readable, stable key for per-company local state). */
69
+ slug: string;
70
+ /** S3 bucket name for this entity */
71
+ bucketName: string;
72
+ /** AWS region */
73
+ region: string;
74
+ /** STS-scoped credentials */
75
+ credentials: VaultCredentials;
76
+ /** When the credentials expire (ISO 8601) */
77
+ expiresAt: string;
78
+ }
79
+
80
+ export interface VaultCredentials {
81
+ accessKeyId: string;
82
+ secretAccessKey: string;
83
+ sessionToken: string;
84
+ }
85
+
86
+ /**
87
+ * Configuration for connecting to the vault-service API.
88
+ */
89
+ export interface VaultServiceConfig {
90
+ /** Vault API base URL (e.g. https://vault-api.example.com) */
91
+ apiUrl: string;
92
+ /** Cognito JWT token for authentication */
93
+ authToken: string;
94
+ /** AWS region for S3 client (defaults to entity region or us-east-1) */
95
+ region?: string;
96
+ }