@hasna/mementos 0.14.88 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/README.md +119 -30
  2. package/bun.lock +4 -65
  3. package/dist/audit-contract.d.ts +87 -0
  4. package/dist/audit-contract.d.ts.map +1 -0
  5. package/dist/cli/commands/info-context.d.ts.map +1 -1
  6. package/dist/cli/commands/info-history.d.ts.map +1 -1
  7. package/dist/cli/commands/info-stale.d.ts.map +1 -1
  8. package/dist/cli/commands/io-backup.d.ts.map +1 -1
  9. package/dist/cli/commands/io-export.d.ts.map +1 -1
  10. package/dist/cli/commands/io-restore.d.ts.map +1 -1
  11. package/dist/cli/commands/memory-cmd-chain.d.ts.map +1 -1
  12. package/dist/cli/commands/memory-cmd-crud.d.ts.map +1 -1
  13. package/dist/cli/commands/memory-cmd-list.d.ts +25 -0
  14. package/dist/cli/commands/memory-cmd-list.d.ts.map +1 -1
  15. package/dist/cli/commands/memory-cmd-recall.d.ts.map +1 -1
  16. package/dist/cli/commands/memory-cmd-search.d.ts.map +1 -1
  17. package/dist/cli/commands/memory-cmd-tail.d.ts.map +1 -1
  18. package/dist/cli/commands/memory-cmd-view.d.ts.map +1 -1
  19. package/dist/cli/commands/memory-cmd-when-to-use.d.ts.map +1 -1
  20. package/dist/cli/commands/project.d.ts.map +1 -1
  21. package/dist/cli/commands/storage.d.ts.map +1 -1
  22. package/dist/cli/commands/system-doctor.d.ts.map +1 -1
  23. package/dist/cli/commands/system-session.d.ts.map +1 -1
  24. package/dist/cli/commands/system-status.d.ts +43 -0
  25. package/dist/cli/commands/system-status.d.ts.map +1 -0
  26. package/dist/cli/commands/system-synthesized-profile.d.ts.map +1 -1
  27. package/dist/cli/commands/system.d.ts.map +1 -1
  28. package/dist/cli/helpers.d.ts.map +1 -1
  29. package/dist/cli/index.js +4964 -2168
  30. package/dist/cli/register-all.d.ts +1 -1
  31. package/dist/cli/register-all.d.ts.map +1 -1
  32. package/dist/db/api-mode.d.ts +145 -37
  33. package/dist/db/api-mode.d.ts.map +1 -1
  34. package/dist/db/api-response-contract.d.ts +23 -0
  35. package/dist/db/api-response-contract.d.ts.map +1 -0
  36. package/dist/db/audit.d.ts +34 -45
  37. package/dist/db/audit.d.ts.map +1 -1
  38. package/dist/db/database.d.ts +1 -0
  39. package/dist/db/database.d.ts.map +1 -1
  40. package/dist/db/locks.d.ts.map +1 -1
  41. package/dist/db/machines.d.ts +50 -13
  42. package/dist/db/machines.d.ts.map +1 -1
  43. package/dist/db/memories.d.ts.map +1 -1
  44. package/dist/db/migrations.d.ts.map +1 -1
  45. package/dist/db/pg-migrate.d.ts.map +1 -1
  46. package/dist/db/pg-migrations.d.ts.map +1 -1
  47. package/dist/db/session-jobs.d.ts +2 -0
  48. package/dist/db/session-jobs.d.ts.map +1 -1
  49. package/dist/db/store-backend.d.ts +8 -2
  50. package/dist/db/store-backend.d.ts.map +1 -1
  51. package/dist/diagnostics/historical-project-registration-receipt.js +299 -50
  52. package/dist/generated/storage-kit/backend.d.ts +12 -2
  53. package/dist/generated/storage-kit/backend.d.ts.map +1 -1
  54. package/dist/generated/storage-kit/index.d.ts +1 -1
  55. package/dist/generated/storage-kit/index.d.ts.map +1 -1
  56. package/dist/generated/storage-kit/migrations.d.ts +21 -0
  57. package/dist/generated/storage-kit/migrations.d.ts.map +1 -1
  58. package/dist/generated/storage-kit/pool.d.ts +2 -5
  59. package/dist/generated/storage-kit/pool.d.ts.map +1 -1
  60. package/dist/generated/storage-kit/query.d.ts +1 -1
  61. package/dist/generated/storage-kit/query.d.ts.map +1 -1
  62. package/dist/index.d.ts +4 -3
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +4328 -1485
  65. package/dist/lib/built-in-hooks.d.ts.map +1 -1
  66. package/dist/lib/config.d.ts.map +1 -1
  67. package/dist/lib/env.d.ts +10 -0
  68. package/dist/lib/env.d.ts.map +1 -0
  69. package/dist/lib/gatherer.d.ts.map +1 -1
  70. package/dist/lib/injection-project.d.ts +10 -0
  71. package/dist/lib/injection-project.d.ts.map +1 -0
  72. package/dist/lib/injector.d.ts +1 -0
  73. package/dist/lib/injector.d.ts.map +1 -1
  74. package/dist/lib/local-opt-in.d.ts +114 -0
  75. package/dist/lib/local-opt-in.d.ts.map +1 -0
  76. package/dist/lib/machine-visibility.d.ts.map +1 -1
  77. package/dist/lib/memory-lock.d.ts.map +1 -1
  78. package/dist/lib/paths.d.ts +9 -0
  79. package/dist/lib/paths.d.ts.map +1 -1
  80. package/dist/lib/profile-synthesizer.d.ts +13 -2
  81. package/dist/lib/profile-synthesizer.d.ts.map +1 -1
  82. package/dist/lib/redact.d.ts +70 -10
  83. package/dist/lib/redact.d.ts.map +1 -1
  84. package/dist/lib/reflection.d.ts.map +1 -1
  85. package/dist/lib/session-queue.d.ts.map +1 -1
  86. package/dist/lib/session-registry.d.ts +38 -1
  87. package/dist/lib/session-registry.d.ts.map +1 -1
  88. package/dist/lib/storage-sync.d.ts +5 -0
  89. package/dist/lib/storage-sync.d.ts.map +1 -1
  90. package/dist/lib/synthesis/index.d.ts.map +1 -1
  91. package/dist/mcp/index.d.ts +1 -1
  92. package/dist/mcp/index.d.ts.map +1 -1
  93. package/dist/mcp/index.js +6215 -2598
  94. package/dist/mcp/profile.d.ts +23 -0
  95. package/dist/mcp/profile.d.ts.map +1 -0
  96. package/dist/mcp/tools/lock-tools.d.ts.map +1 -1
  97. package/dist/mcp/tools/memory-audit.d.ts +1 -1
  98. package/dist/mcp/tools/memory-audit.d.ts.map +1 -1
  99. package/dist/mcp/tools/memory-crud.d.ts.map +1 -1
  100. package/dist/mcp/tools/memory-inject.d.ts.map +1 -1
  101. package/dist/mcp/tools/project-tools.d.ts.map +1 -1
  102. package/dist/mcp/tools/session-tools.d.ts.map +1 -1
  103. package/dist/mcp/tools/tool-registry.d.ts +22 -19
  104. package/dist/mcp/tools/tool-registry.d.ts.map +1 -1
  105. package/dist/mcp/tools/utility-tools.d.ts +2 -1
  106. package/dist/mcp/tools/utility-tools.d.ts.map +1 -1
  107. package/dist/project-registration.js +549 -205
  108. package/dist/sdk/index.d.ts +282 -14
  109. package/dist/sdk/index.d.ts.map +1 -1
  110. package/dist/sdk/index.js +1691 -35
  111. package/dist/server/auth.d.ts +2 -0
  112. package/dist/server/auth.d.ts.map +1 -1
  113. package/dist/server/helpers.d.ts +21 -7
  114. package/dist/server/helpers.d.ts.map +1 -1
  115. package/dist/server/index.d.ts +2 -0
  116. package/dist/server/index.d.ts.map +1 -1
  117. package/dist/server/index.js +6040 -2475
  118. package/dist/server/openapi.d.ts.map +1 -1
  119. package/dist/server/routes/audit.d.ts +2 -0
  120. package/dist/server/routes/audit.d.ts.map +1 -0
  121. package/dist/server/routes/machines.d.ts +2 -0
  122. package/dist/server/routes/machines.d.ts.map +1 -0
  123. package/dist/server/routes/system-sessions.d.ts.map +1 -1
  124. package/dist/server/routes/system-synthesis.d.ts.map +1 -1
  125. package/dist/storage.d.ts.map +1 -1
  126. package/dist/storage.js +78 -129
  127. package/dist/types/index.d.ts +3 -0
  128. package/dist/types/index.d.ts.map +1 -1
  129. package/docker-entrypoint.sh +7 -0
  130. package/hasna.contract.json +19 -7
  131. package/package.json +12 -11
  132. package/postinstall.mjs +52 -1
  133. package/dashboard/bun.lock +0 -519
  134. package/dist/cli/__fixtures__/clean-fallback-stub-server.d.ts +0 -2
  135. package/dist/cli/__fixtures__/clean-fallback-stub-server.d.ts.map +0 -1
  136. package/dist/cli/__fixtures__/io-restore-stub-server.d.ts +0 -2
  137. package/dist/cli/__fixtures__/io-restore-stub-server.d.ts.map +0 -1
  138. package/dist/cli/brains.d.ts +0 -3
  139. package/dist/cli/brains.d.ts.map +0 -1
  140. package/dist/db/__fixtures__/fail-closed-stub-server.d.ts +0 -2
  141. package/dist/db/__fixtures__/fail-closed-stub-server.d.ts.map +0 -1
  142. package/dist/db/__fixtures__/list-filter-capture-server.d.ts +0 -2
  143. package/dist/db/__fixtures__/list-filter-capture-server.d.ts.map +0 -1
  144. package/dist/db/__fixtures__/list-filter-client-runner.d.ts +0 -2
  145. package/dist/db/__fixtures__/list-filter-client-runner.d.ts.map +0 -1
  146. package/dist/lib/retired-storage-mode.d.ts +0 -11
  147. package/dist/lib/retired-storage-mode.d.ts.map +0 -1
  148. package/dist/test-support/memories-page-stub-server.d.ts +0 -2
  149. package/dist/test-support/memories-page-stub-server.d.ts.map +0 -1
  150. package/dist/test-support/memories-page-stub.d.ts +0 -28
  151. package/dist/test-support/memories-page-stub.d.ts.map +0 -1
  152. package/dist/test-support/pg-sync-stub-worker.d.ts +0 -2
  153. package/dist/test-support/pg-sync-stub-worker.d.ts.map +0 -1
  154. package/dist/test-support/preload-local-store.d.ts +0 -2
  155. package/dist/test-support/preload-local-store.d.ts.map +0 -1
  156. package/dist/test-support/project-authority-identity.d.ts +0 -8
  157. package/dist/test-support/project-authority-identity.d.ts.map +0 -1
  158. package/dist/test-support/store-isolation.d.ts +0 -108
  159. package/dist/test-support/store-isolation.d.ts.map +0 -1
  160. package/dist/test-support/strip-ansi.d.ts +0 -3
  161. package/dist/test-support/strip-ansi.d.ts.map +0 -1
package/dist/sdk/index.js CHANGED
@@ -1,5 +1,1077 @@
1
1
  // @bun
2
+ // src/audit-contract.ts
3
+ var AUDIT_TRAIL_CONTRACT = "mementos.audit.trail.v1";
4
+ var AUDIT_EXPORT_CONTRACT = "mementos.audit.export.v1";
5
+ var AUDIT_STATS_CONTRACT = "mementos.audit.stats.v1";
6
+ var AUDIT_DEFAULT_LIMIT = 50;
7
+ var AUDIT_OPERATIONS = [
8
+ "create",
9
+ "update",
10
+ "delete",
11
+ "archive",
12
+ "restore",
13
+ "read"
14
+ ];
15
+
16
+ class AuditContractError extends Error {
17
+ code = "MEMENTOS_AUDIT_CONTRACT";
18
+ constructor(message) {
19
+ super(message);
20
+ this.name = "AuditContractError";
21
+ }
22
+ }
23
+ var ISO_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
24
+ var MD5_HEX = /^[0-9a-f]{32}$/;
25
+ var ENTRY_KEYS = new Set([
26
+ "id",
27
+ "memory_id",
28
+ "memory_key",
29
+ "operation",
30
+ "agent_id",
31
+ "old_value_hash",
32
+ "new_value_hash",
33
+ "changes",
34
+ "created_at"
35
+ ]);
36
+ var PAGE_KEYS = new Set([
37
+ "contract",
38
+ "entries",
39
+ "count",
40
+ "total",
41
+ "limit",
42
+ "cursor",
43
+ "next_cursor",
44
+ "consumed",
45
+ "has_more",
46
+ "complete",
47
+ "snapshot_at",
48
+ "filters",
49
+ "sort"
50
+ ]);
51
+ function object(value, label) {
52
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
53
+ throw new AuditContractError(`${label} must be a JSON object`);
54
+ }
55
+ return value;
56
+ }
57
+ function exactKeys(value, allowed, label) {
58
+ const unexpected = Object.keys(value).filter((key) => !allowed.has(key));
59
+ if (unexpected.length)
60
+ throw new AuditContractError(`${label} has unexpected field '${unexpected[0]}'`);
61
+ }
62
+ function string(value, label, options = {}) {
63
+ if (options.nullable && value === null)
64
+ return null;
65
+ if (typeof value !== "string" || value.length === 0 || value.length > (options.max ?? 2048) || /[\u0000-\u001f\u007f]/.test(value)) {
66
+ throw new AuditContractError(`${label} must be a non-empty printable string of at most ${options.max ?? 2048} characters`);
67
+ }
68
+ return value;
69
+ }
70
+ function cursorString(value, label) {
71
+ if (value === null)
72
+ return null;
73
+ if (typeof value !== "string" || !value || value.length > 4096 || !/^[A-Za-z0-9_-]+$/.test(value)) {
74
+ throw new AuditContractError(`${label} must be a bounded base64url cursor or null`);
75
+ }
76
+ return value;
77
+ }
78
+ function integer(value, label, minimum = 0) {
79
+ if (!Number.isSafeInteger(value) || value < minimum) {
80
+ throw new AuditContractError(`${label} must be a safe integer >= ${minimum}`);
81
+ }
82
+ return value;
83
+ }
84
+ function boolean(value, label) {
85
+ if (typeof value !== "boolean")
86
+ throw new AuditContractError(`${label} must be a boolean`);
87
+ return value;
88
+ }
89
+ function canonicalAuditTimestamp(value, label) {
90
+ if (value instanceof Date)
91
+ value = value.toISOString();
92
+ if (typeof value !== "string")
93
+ throw new AuditContractError(`${label} must be a canonical UTC timestamp`);
94
+ const candidate = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d{1,3})?$/.test(value) ? `${value.replace(" ", "T")}${value.includes(".") ? "" : ".000"}Z` : value;
95
+ if (!ISO_UTC.test(candidate))
96
+ throw new AuditContractError(`${label} must use YYYY-MM-DDTHH:mm:ss.sssZ`);
97
+ const parsed = new Date(candidate);
98
+ if (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== candidate) {
99
+ throw new AuditContractError(`${label} must be a real calendar timestamp`);
100
+ }
101
+ return candidate;
102
+ }
103
+ function auditOperation(value, label = "operation") {
104
+ if (typeof value !== "string" || !AUDIT_OPERATIONS.includes(value)) {
105
+ throw new AuditContractError(`${label} must be one of: ${AUDIT_OPERATIONS.join(", ")}`);
106
+ }
107
+ return value;
108
+ }
109
+ function nullableHash(value, label) {
110
+ if (value === null)
111
+ return null;
112
+ if (typeof value !== "string" || !MD5_HEX.test(value)) {
113
+ throw new AuditContractError(`${label} must be a lowercase md5 hex digest or null`);
114
+ }
115
+ return value;
116
+ }
117
+ function record(value, label) {
118
+ return object(value, label);
119
+ }
120
+ function validateAuditEntry(value, label = "audit entry") {
121
+ const row = object(value, label);
122
+ exactKeys(row, ENTRY_KEYS, label);
123
+ return {
124
+ id: string(row.id, `${label}.id`, { max: 512 }),
125
+ memory_id: string(row.memory_id, `${label}.memory_id`, { max: 512 }),
126
+ memory_key: string(row.memory_key, `${label}.memory_key`, { nullable: true, max: 4096 }),
127
+ operation: auditOperation(row.operation, `${label}.operation`),
128
+ agent_id: string(row.agent_id, `${label}.agent_id`, { nullable: true, max: 512 }),
129
+ old_value_hash: nullableHash(row.old_value_hash, `${label}.old_value_hash`),
130
+ new_value_hash: nullableHash(row.new_value_hash, `${label}.new_value_hash`),
131
+ changes: record(row.changes, `${label}.changes`),
132
+ created_at: canonicalAuditTimestamp(row.created_at, `${label}.created_at`)
133
+ };
134
+ }
135
+ function nullableTimestamp(value, label) {
136
+ return value === null ? null : canonicalAuditTimestamp(value, label);
137
+ }
138
+ function validateFilters(value) {
139
+ const filters = object(value, "audit page filters");
140
+ const expected = new Set(["memory_id", "since", "until", "operation", "agent_id"]);
141
+ exactKeys(filters, expected, "audit page filters");
142
+ const result = {
143
+ memory_id: string(filters.memory_id, "filters.memory_id", { nullable: true, max: 512 }),
144
+ since: nullableTimestamp(filters.since, "filters.since"),
145
+ until: nullableTimestamp(filters.until, "filters.until"),
146
+ operation: filters.operation === null ? null : auditOperation(filters.operation, "filters.operation"),
147
+ agent_id: string(filters.agent_id, "filters.agent_id", { nullable: true, max: 512 })
148
+ };
149
+ if (result.since && result.until && result.since > result.until) {
150
+ throw new AuditContractError("filters.since must not be after filters.until");
151
+ }
152
+ return result;
153
+ }
154
+ function compareEntryOrder(left, right) {
155
+ if (left.created_at !== right.created_at)
156
+ return left.created_at > right.created_at ? -1 : 1;
157
+ return left.id > right.id ? -1 : left.id < right.id ? 1 : 0;
158
+ }
159
+ function validateAuditPage(value, expected) {
160
+ const page = object(value, "audit page");
161
+ exactKeys(page, PAGE_KEYS, "audit page");
162
+ if (page.contract !== expected.contract)
163
+ throw new AuditContractError(`expected contract '${expected.contract}'`);
164
+ const entries = Array.isArray(page.entries) ? page.entries.map((entry, index) => validateAuditEntry(entry, `entries[${index}]`)) : (() => {
165
+ throw new AuditContractError("entries must be an array");
166
+ })();
167
+ const count = integer(page.count, "count");
168
+ const total = integer(page.total, "total");
169
+ const limit = integer(page.limit, "limit", 1);
170
+ const requestedLimit = integer(expected.limit, "requested limit", 1);
171
+ if (limit !== requestedLimit)
172
+ throw new AuditContractError("limit receipt does not match the request");
173
+ const consumed = integer(page.consumed, "consumed");
174
+ const cursor = cursorString(page.cursor, "cursor");
175
+ const nextCursor = cursorString(page.next_cursor, "next_cursor");
176
+ const hasMore = boolean(page.has_more, "has_more");
177
+ const complete = boolean(page.complete, "complete");
178
+ const snapshotAt = canonicalAuditTimestamp(page.snapshot_at, "snapshot_at");
179
+ const filters = validateFilters(page.filters);
180
+ const sort = object(page.sort, "sort");
181
+ exactKeys(sort, new Set(["field", "direction", "tie_breaker"]), "sort");
182
+ if (sort.field !== "created_at" || sort.direction !== "desc" || sort.tie_breaker !== "id") {
183
+ throw new AuditContractError("sort must be created_at desc with id tie-breaker");
184
+ }
185
+ if (cursor !== expected.cursor)
186
+ throw new AuditContractError("cursor receipt does not match the request");
187
+ for (const key of ["memory_id", "since", "until", "operation", "agent_id"]) {
188
+ if (filters[key] !== expected.filters[key]) {
189
+ throw new AuditContractError(`filters.${key} receipt does not match the request`);
190
+ }
191
+ }
192
+ if (count !== entries.length || count > limit || total < count || consumed < count || consumed > total) {
193
+ throw new AuditContractError("count/limit/consumed/total fields are inconsistent");
194
+ }
195
+ if (hasMore !== consumed < total)
196
+ throw new AuditContractError("has_more does not match consumed/total");
197
+ if (hasMore !== (nextCursor !== null))
198
+ throw new AuditContractError("next_cursor does not match has_more");
199
+ if (total > 0 && count === 0)
200
+ throw new AuditContractError("non-empty audit result must make progress on every page");
201
+ if (hasMore && nextCursor === cursor)
202
+ throw new AuditContractError("next_cursor must advance beyond the request cursor");
203
+ const shouldBeComplete = cursor === null && !hasMore && consumed === total;
204
+ if (complete !== shouldBeComplete)
205
+ throw new AuditContractError("complete is not truthful for this page");
206
+ if (cursor === null && consumed !== count)
207
+ throw new AuditContractError("initial page consumed must equal count");
208
+ for (let index = 1;index < entries.length; index++) {
209
+ if (compareEntryOrder(entries[index - 1], entries[index]) >= 0) {
210
+ throw new AuditContractError("entries are not strictly ordered by created_at desc, id desc");
211
+ }
212
+ }
213
+ const ids = new Set;
214
+ for (const entry of entries) {
215
+ if (ids.has(entry.id))
216
+ throw new AuditContractError(`duplicate audit entry id '${entry.id}'`);
217
+ ids.add(entry.id);
218
+ if (filters.memory_id && entry.memory_id !== filters.memory_id)
219
+ throw new AuditContractError("entry violates memory_id filter");
220
+ if (filters.operation && entry.operation !== filters.operation)
221
+ throw new AuditContractError("entry violates operation filter");
222
+ if (filters.agent_id && entry.agent_id !== filters.agent_id)
223
+ throw new AuditContractError("entry violates agent_id filter");
224
+ if (filters.since && entry.created_at < filters.since)
225
+ throw new AuditContractError("entry violates since filter");
226
+ if (filters.until && entry.created_at > filters.until)
227
+ throw new AuditContractError("entry violates until filter");
228
+ if (entry.created_at > snapshotAt)
229
+ throw new AuditContractError("entry is newer than the snapshot boundary");
230
+ }
231
+ return {
232
+ contract: expected.contract,
233
+ entries,
234
+ count,
235
+ total,
236
+ limit,
237
+ cursor,
238
+ next_cursor: nextCursor,
239
+ consumed,
240
+ has_more: hasMore,
241
+ complete,
242
+ snapshot_at: snapshotAt,
243
+ filters,
244
+ sort: { field: "created_at", direction: "desc", tie_breaker: "id" }
245
+ };
246
+ }
247
+ function validateAuditStats(value) {
248
+ const stats = object(value, "audit stats");
249
+ exactKeys(stats, new Set(["contract", "total_entries", "by_operation", "recent_24h", "snapshot_at"]), "audit stats");
250
+ if (stats.contract !== AUDIT_STATS_CONTRACT)
251
+ throw new AuditContractError(`expected contract '${AUDIT_STATS_CONTRACT}'`);
252
+ const total = integer(stats.total_entries, "total_entries");
253
+ const recent = integer(stats.recent_24h, "recent_24h");
254
+ const byOperationObject = object(stats.by_operation, "by_operation");
255
+ exactKeys(byOperationObject, new Set(AUDIT_OPERATIONS), "by_operation");
256
+ const byOperation = Object.fromEntries(AUDIT_OPERATIONS.map((operation) => [operation, integer(byOperationObject[operation], `by_operation.${operation}`)]));
257
+ if (Object.values(byOperation).reduce((sum, count) => sum + count, 0) !== total) {
258
+ throw new AuditContractError("by_operation counts do not sum to total_entries");
259
+ }
260
+ if (recent > total)
261
+ throw new AuditContractError("recent_24h exceeds total_entries");
262
+ return {
263
+ contract: AUDIT_STATS_CONTRACT,
264
+ total_entries: total,
265
+ by_operation: byOperation,
266
+ recent_24h: recent,
267
+ snapshot_at: canonicalAuditTimestamp(stats.snapshot_at, "snapshot_at")
268
+ };
269
+ }
270
+
271
+ // ../../node_modules/.bun/@hasna+contracts@1.0.2+e8014c875821e0be/node_modules/@hasna/contracts/dist/client/transport.js
272
+ import { isIP } from "net";
273
+ import { spawnSync } from "child_process";
274
+ import { closeSync, fstatSync, openSync, readFileSync } from "fs";
275
+ import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
276
+ import { createRequire } from "module";
277
+ import { hostname as osHostname } from "os";
278
+ import { isAbsolute, join } from "path";
279
+ function envToken(name) {
280
+ return name.toUpperCase().replace(/-/g, "_");
281
+ }
282
+ function clientTransportEnvKeys(name) {
283
+ const envSegment = envToken(name);
284
+ return {
285
+ apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
286
+ apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
287
+ };
288
+ }
289
+ function credentialOverrideEnvKey(name) {
290
+ return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
291
+ }
292
+ var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
293
+ function credentialPointerEnvKey(name) {
294
+ return `HASNA_${envToken(name)}_API_KEY_REF`;
295
+ }
296
+
297
+ class CredentialResolutionError extends Error {
298
+ appName;
299
+ attempted;
300
+ constructor(appName, message, attempted) {
301
+ super(message);
302
+ this.name = "CredentialResolutionError";
303
+ this.appName = appName;
304
+ this.attempted = attempted;
305
+ }
306
+ }
307
+
308
+ class CredentialFileUnsafeError extends Error {
309
+ path;
310
+ constructor(path, reason) {
311
+ super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
312
+ this.name = "CredentialFileUnsafeError";
313
+ this.path = path;
314
+ }
315
+ }
316
+ var HASNA_HOME_ENV_KEY = "HASNA_HOME";
317
+ var HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME";
318
+ var KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION";
319
+ var HASNA_HOME_DIR = ".hasna";
320
+ var CONFIG_SUBDIR = "config";
321
+ var CREDENTIALS_FILE = "credentials";
322
+ var KEYCHAIN_SECURITY_BIN = "/usr/bin/security";
323
+ var KEYCHAIN_SERVICE_PREFIX = "hasna.credentials";
324
+ var KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44;
325
+ var KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4;
326
+ var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
327
+ var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
328
+ var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
329
+ var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
330
+ var VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
331
+ function homeDir(env) {
332
+ const home = env.HOME?.trim();
333
+ return home ? home : null;
334
+ }
335
+ function absoluteOverride(env, key) {
336
+ const value = env[key]?.trim();
337
+ return value && isAbsolute(value) ? value : null;
338
+ }
339
+ function hasnaHomeDir(env) {
340
+ const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
341
+ if (override)
342
+ return override;
343
+ const home = homeDir(env);
344
+ return home ? join(home, HASNA_HOME_DIR) : null;
345
+ }
346
+ function appConfigDir(name, env) {
347
+ const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
348
+ if (configRoot)
349
+ return join(configRoot, name);
350
+ const root = hasnaHomeDir(env);
351
+ return root ? join(root, name, CONFIG_SUBDIR) : null;
352
+ }
353
+ function credentialDiskSourceList(name, env, profile = null) {
354
+ if (!SAFE_APP_SLUG.test(name))
355
+ return [];
356
+ const directory = appConfigDir(name, env);
357
+ if (!directory)
358
+ return [];
359
+ const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
360
+ return [{ path: join(directory, file), tier: "disk" }];
361
+ }
362
+ function credentialDiskSources(name, env) {
363
+ return credentialDiskSourceList(name, env, null).map((s) => s.path);
364
+ }
365
+ function profileDiskSources(name, env, profile) {
366
+ return credentialDiskSourceList(name, env, profile).map((s) => s.path);
367
+ }
368
+ function parseEnvFile(text) {
369
+ const values = new Map;
370
+ const unusable = new Set;
371
+ for (const rawLine of text.split(/\r?\n/)) {
372
+ const line = rawLine.trim();
373
+ if (line.length === 0 || line.startsWith("#"))
374
+ continue;
375
+ const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
376
+ const equals = withoutExport.indexOf("=");
377
+ if (equals <= 0)
378
+ continue;
379
+ const key = withoutExport.slice(0, equals).trim();
380
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
381
+ continue;
382
+ let value = withoutExport.slice(equals + 1).trim();
383
+ const quote = value[0];
384
+ if (quote === '"' || quote === "'") {
385
+ if (value.length < 2 || !value.endsWith(quote)) {
386
+ unusable.add(key);
387
+ continue;
388
+ }
389
+ value = value.slice(1, -1);
390
+ }
391
+ if (value.trim().length === 0) {
392
+ unusable.add(key);
393
+ continue;
394
+ }
395
+ if (values.has(key) && values.get(key) !== value)
396
+ unusable.add(key);
397
+ values.set(key, value);
398
+ }
399
+ return { values, unusable };
400
+ }
401
+ function configFileModeAllowed(mode) {
402
+ const permissions = mode & 4095;
403
+ return permissions === 256 || permissions === 384;
404
+ }
405
+ function configFileReadsCoherent(before, after) {
406
+ return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
407
+ }
408
+ function readAppConfigFile(path) {
409
+ const unsafe = (reason) => {
410
+ throw new CredentialFileUnsafeError(path, reason);
411
+ };
412
+ let fd = -1;
413
+ try {
414
+ fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
415
+ } catch (error) {
416
+ const code = error.code;
417
+ if (code === "ENOENT" || code === "ENOTDIR")
418
+ return null;
419
+ if (code === "ELOOP")
420
+ unsafe("the path is a symlink");
421
+ unsafe(`the path could not be opened (${code ?? "unknown error"})`);
422
+ }
423
+ try {
424
+ const before = fstatSync(fd);
425
+ if (!before.isFile())
426
+ unsafe("the path is not a regular file");
427
+ if (!configFileModeAllowed(before.mode)) {
428
+ unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
429
+ }
430
+ const uid = process.getuid?.() ?? process.geteuid?.();
431
+ if (uid !== undefined && before.uid !== uid)
432
+ unsafe("the file is not owned by the current user");
433
+ if (before.size > MAX_CREDENTIAL_FILE_BYTES)
434
+ unsafe("the file exceeds the size limit");
435
+ const bytes = readFileSync(fd);
436
+ const after = fstatSync(fd);
437
+ if (!configFileReadsCoherent(before, after)) {
438
+ unsafe("the file changed while being read");
439
+ }
440
+ return parseEnvFile(bytes.toString("utf8"));
441
+ } finally {
442
+ if (fd !== -1)
443
+ closeSync(fd);
444
+ }
445
+ }
446
+ function readCredentialFile(path, apiKeyKeys) {
447
+ const parsed = readAppConfigFile(path);
448
+ if (!parsed)
449
+ return null;
450
+ for (const key of apiKeyKeys) {
451
+ if (parsed.unusable.has(key)) {
452
+ throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
453
+ }
454
+ }
455
+ const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
456
+ if (new Set(values).size > 1) {
457
+ throw new CredentialFileUnsafeError(path, "credential aliases disagree");
458
+ }
459
+ return values[0] ?? null;
460
+ }
461
+ var CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
462
+ function appConfigDiskValue(name, env, keys) {
463
+ const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
464
+ if (wanted.length === 0)
465
+ return null;
466
+ for (const path of credentialDiskSources(name, env)) {
467
+ const parsed = readAppConfigFile(path);
468
+ if (!parsed)
469
+ continue;
470
+ if (wanted.some((key) => parsed.unusable.has(key))) {
471
+ return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
472
+ }
473
+ const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
474
+ if (new Set(values).size > 1)
475
+ throw new CredentialFileUnsafeError(path, "configuration aliases disagree");
476
+ for (const key of wanted) {
477
+ if (parsed.unusable.has(key))
478
+ return { key, value: "", path, unusable: true };
479
+ const value = parsed.values.get(key)?.trim();
480
+ if (value)
481
+ return { key, value, path };
482
+ }
483
+ }
484
+ return null;
485
+ }
486
+ function assertUsableCredential(appName, source, value) {
487
+ if (VAULT_POINTER_SHAPE.test(value)) {
488
+ throw new CredentialResolutionError(appName, `The credential from ${source} looks like a secrets-vault pointer (a path-shaped reference like ` + `'namespace/app/live/api_key'). A vault path is NEVER accepted as a literal API key. ` + `Use ${credentialPointerEnvKey(appName)} to resolve the key through the vault, or provide the actual key value.`, [source]);
489
+ }
490
+ if (!ILLEGAL_IN_HEADER_VALUE.test(value))
491
+ return;
492
+ throw new CredentialResolutionError(appName, `The credential from ${source} contains characters that cannot be sent in an HTTP header ` + `(a control character or non-ASCII byte). A file written with CR-only line endings is the usual ` + `cause. Rewrite that credential file with one LF-terminated KEY=value line. ` + `The value is not shown here, and is deliberately never logged.`, [source]);
493
+ }
494
+ var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
495
+ var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
496
+ function sealCredential(fields) {
497
+ const { apiKey } = fields;
498
+ const visible = {
499
+ tier: fields.tier,
500
+ source: fields.source,
501
+ deliberate: fields.deliberate,
502
+ diskCandidates: Object.freeze([...fields.diskCandidates]),
503
+ warning: fields.warning
504
+ };
505
+ const sealed = { ...visible };
506
+ Object.defineProperty(sealed, "apiKey", {
507
+ value: apiKey,
508
+ enumerable: false,
509
+ writable: false,
510
+ configurable: false
511
+ });
512
+ if (fields.pointerVaultKey !== undefined) {
513
+ Object.defineProperty(sealed, "pointerVaultKey", {
514
+ value: fields.pointerVaultKey,
515
+ enumerable: false,
516
+ writable: false,
517
+ configurable: false
518
+ });
519
+ }
520
+ Object.defineProperty(sealed, INSPECT_CUSTOM, {
521
+ value: () => ({ ...visible, apiKey: "[redacted]" }),
522
+ enumerable: false,
523
+ writable: false,
524
+ configurable: false
525
+ });
526
+ Object.defineProperty(sealed, CREDENTIAL_SEAL, {
527
+ value: true,
528
+ enumerable: false,
529
+ writable: false,
530
+ configurable: false
531
+ });
532
+ return Object.freeze(sealed);
533
+ }
534
+ function firstEnvValue(env, keys) {
535
+ for (const key of keys) {
536
+ if (!Object.prototype.hasOwnProperty.call(env, key))
537
+ continue;
538
+ const value = env[key]?.trim();
539
+ if (value)
540
+ return { key, value };
541
+ }
542
+ return null;
543
+ }
544
+ var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
545
+ function isAmbientEnvironment(env) {
546
+ return env === process.env || env[AMBIENT_ENVIRONMENT] === true;
547
+ }
548
+ function defaultKeychainRunner(argv) {
549
+ const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {
550
+ encoding: "utf8",
551
+ stdio: ["ignore", "pipe", "pipe"],
552
+ timeout: KEYCHAIN_SPAWN_TIMEOUT_MS
553
+ });
554
+ return {
555
+ status: result.status,
556
+ stdout: result.stdout ?? "",
557
+ stderr: result.error ? result.error.message : result.stderr ?? ""
558
+ };
559
+ }
560
+ function keychainTierEnabled(env, options) {
561
+ if ((options.platform ?? process.platform) !== "darwin")
562
+ return false;
563
+ if (options.enabled !== undefined)
564
+ return options.enabled;
565
+ return options.run !== undefined || isAmbientEnvironment(env);
566
+ }
567
+ function keychainAccount(env, options) {
568
+ const station = env[KEYCHAIN_STATION_ENV_KEY]?.trim();
569
+ if (station)
570
+ return station;
571
+ const host = (options.hostname ?? osHostname)().split(".")[0]?.trim() ?? "";
572
+ if (host)
573
+ return host;
574
+ const user = env.USER?.trim();
575
+ return user || null;
576
+ }
577
+ function keychainFailureHint(text) {
578
+ const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
579
+ const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
580
+ return clean ? `: ${clean}` : "";
581
+ }
582
+ function readKeychainItem(name, env, kind, options) {
583
+ if (!SAFE_APP_SLUG.test(name) || !keychainTierEnabled(env, options))
584
+ return null;
585
+ const account = keychainAccount(env, options);
586
+ if (!account)
587
+ return null;
588
+ const service = `${KEYCHAIN_SERVICE_PREFIX}.${name}.${kind}`;
589
+ const source = `keychain:${service}@${account}`;
590
+ const run = options.run ?? defaultKeychainRunner;
591
+ let result;
592
+ try {
593
+ result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
594
+ } catch (error) {
595
+ const reason = keychainFailureHint(error instanceof Error ? error.message : String(error));
596
+ throw new CredentialResolutionError(name, `The Keychain lookup for ${source} could not run${reason}. A Keychain failure is never resolved ` + `around: fix the keychain, or delete the item to fall through to the credential on disk.`, [source]);
597
+ }
598
+ if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS)
599
+ return null;
600
+ if (result.status !== 0) {
601
+ throw new CredentialResolutionError(name, `The Keychain lookup for ${source} failed (security exited ` + `${result.status ?? "without a status"}${keychainFailureHint(result.stderr)}). A Keychain item that ` + `exists but cannot be read is never resolved around: unlock the keychain, run from a session that ` + `may use it, or delete the item to fall through to the credential on disk.`, [source]);
602
+ }
603
+ const value = result.stdout.trim();
604
+ if (!value) {
605
+ throw new CredentialResolutionError(name, `${source} exists but holds an empty value; a declared item never falls through to another ` + `identity. Store a value in it or delete the item.`, [source]);
606
+ }
607
+ return { value, source };
608
+ }
609
+ function keychainConfigValue(name, env, options = {}) {
610
+ return readKeychainItem(name, env, "api-url", options);
611
+ }
612
+ function snapshotClientEnvironment(name, env) {
613
+ const keys = clientTransportEnvKeys(name);
614
+ const ambient = isAmbientEnvironment(env);
615
+ const snapshot = Object.create(null);
616
+ for (const key of [
617
+ ...keys.apiUrlKeys,
618
+ ...keys.apiKeyKeys,
619
+ credentialOverrideEnvKey(name),
620
+ credentialPointerEnvKey(name),
621
+ CREDENTIAL_PROFILE_ENV_KEY,
622
+ "HOME",
623
+ HASNA_HOME_ENV_KEY,
624
+ HASNA_CONFIG_HOME_ENV_KEY,
625
+ KEYCHAIN_STATION_ENV_KEY,
626
+ "USER"
627
+ ]) {
628
+ const descriptor = Object.getOwnPropertyDescriptor(env, key);
629
+ if (!descriptor)
630
+ continue;
631
+ if (!("value" in descriptor)) {
632
+ throw new CredentialResolutionError(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
633
+ }
634
+ if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
635
+ throw new CredentialResolutionError(name, `${key} must be a string data property.`, [key]);
636
+ }
637
+ snapshot[key] = descriptor.value;
638
+ }
639
+ if (ambient) {
640
+ Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT, {
641
+ value: true,
642
+ enumerable: false,
643
+ writable: false,
644
+ configurable: false
645
+ });
646
+ }
647
+ return Object.freeze(snapshot);
648
+ }
649
+ function resolveCredential(name, env, options = {}) {
650
+ env = snapshotClientEnvironment(name, env);
651
+ const { apiKeyKeys } = clientTransportEnvKeys(name);
652
+ const diskPaths = credentialDiskSources(name, env);
653
+ if (options.apiKey !== undefined) {
654
+ const explicitKey = options.apiKey.trim();
655
+ if (!explicitKey) {
656
+ throw new CredentialResolutionError(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
657
+ }
658
+ assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
659
+ return sealCredential({
660
+ apiKey: explicitKey,
661
+ tier: "argument",
662
+ source: "explicit apiKey argument",
663
+ deliberate: true,
664
+ diskCandidates: diskPaths,
665
+ warning: null
666
+ });
667
+ }
668
+ const overrideKeyName = credentialOverrideEnvKey(name);
669
+ const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
670
+ if (overrideRaw !== undefined) {
671
+ const override = overrideRaw.trim();
672
+ if (!override) {
673
+ throw new CredentialResolutionError(name, `${overrideKeyName} is set but empty. It is a deliberate override, so it is not resolved around: ` + `either give it a real key or unset it to fall back to the credential on disk.`, [overrideKeyName]);
674
+ }
675
+ assertUsableCredential(name, overrideKeyName, override);
676
+ return sealCredential({
677
+ apiKey: override,
678
+ tier: "override",
679
+ source: overrideKeyName,
680
+ deliberate: true,
681
+ diskCandidates: diskPaths,
682
+ warning: null
683
+ });
684
+ }
685
+ const pointerKeyName = credentialPointerEnvKey(name);
686
+ const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
687
+ if (pointerRaw !== undefined) {
688
+ const pointer = pointerRaw.trim();
689
+ if (!pointer) {
690
+ throw new CredentialResolutionError(name, `${pointerKeyName} is set but empty. It is a deliberate vault pointer, so it is not resolved around: ` + `either give it a vault item key or unset it to fall back to the credential on disk.`, [pointerKeyName]);
691
+ }
692
+ if (!VAULT_POINTER_SHAPE.test(pointer)) {
693
+ throw new CredentialResolutionError(name, `${pointerKeyName} must name a vault ITEM KEY (a path-shaped reference like ` + `'namespace/app/live/api_key'), not a credential value. A pointer that carries a literal is refused.`, [pointerKeyName]);
694
+ }
695
+ return sealCredential({
696
+ apiKey: "",
697
+ pointerVaultKey: pointer,
698
+ tier: "pointer",
699
+ source: pointerKeyName,
700
+ deliberate: true,
701
+ diskCandidates: diskPaths,
702
+ warning: null
703
+ });
704
+ }
705
+ if (options.profile !== undefined && !options.profile.trim()) {
706
+ throw new CredentialResolutionError(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
707
+ }
708
+ const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY) ? env[CREDENTIAL_PROFILE_ENV_KEY] : undefined;
709
+ if (profileRaw !== undefined && !profileRaw.trim()) {
710
+ throw new CredentialResolutionError(name, `${CREDENTIAL_PROFILE_ENV_KEY} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY]);
711
+ }
712
+ const profile = options.profile?.trim() || profileRaw?.trim();
713
+ if (profile) {
714
+ const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
715
+ if (!SAFE_PROFILE.test(profile)) {
716
+ throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
717
+ }
718
+ const paths = profileDiskSources(name, env, profile);
719
+ for (const path of paths) {
720
+ const value = readCredentialFile(path, apiKeyKeys);
721
+ if (value) {
722
+ assertUsableCredential(name, path, value);
723
+ return sealCredential({
724
+ apiKey: value,
725
+ tier: "profile",
726
+ source: path,
727
+ deliberate: true,
728
+ diskCandidates: paths,
729
+ warning: null
730
+ });
731
+ }
732
+ }
733
+ throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
734
+ }
735
+ const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
736
+ const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
737
+ if (blankEnv) {
738
+ throw new CredentialResolutionError(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
739
+ }
740
+ if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
741
+ throw new CredentialResolutionError(name, `${definedEnvEntries.map((entry) => entry.key).join(" and ")} disagree; credential aliases must be identical or only one may be set.`, definedEnvEntries.map((entry) => entry.key));
742
+ }
743
+ const envHit = firstEnvValue(env, apiKeyKeys);
744
+ const keychainHit = readKeychainItem(name, env, "api-key", options.keychain ?? {});
745
+ if (keychainHit) {
746
+ assertUsableCredential(name, keychainHit.source, keychainHit.value);
747
+ const warning = envHit && envHit.value !== keychainHit.value ? `Credential sources disagree for '${name}': ${keychainHit.source} and ${envHit.key} hold ` + `different keys. ${keychainHit.source} wins, because the Keychain is re-read on every call while ` + `an environment variable is a snapshot. Reconcile them \u2014 a rotation that updated only one leaves ` + `the other to fail 401 wherever it is loaded first.` : null;
748
+ return sealCredential({
749
+ apiKey: keychainHit.value,
750
+ tier: "keychain",
751
+ source: keychainHit.source,
752
+ deliberate: false,
753
+ diskCandidates: diskPaths,
754
+ warning
755
+ });
756
+ }
757
+ const diskSourceList = credentialDiskSourceList(name, env, null);
758
+ const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
759
+ if (diskHits.length > 0) {
760
+ const winner = diskHits[0];
761
+ assertUsableCredential(name, winner.src.path, winner.value);
762
+ const divergentSources = [
763
+ ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
764
+ ...envHit && envHit.value !== winner.value ? [envHit.key] : []
765
+ ];
766
+ const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.src.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.src.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
767
+ return sealCredential({
768
+ apiKey: winner.value,
769
+ tier: winner.src.tier,
770
+ source: winner.src.path,
771
+ deliberate: false,
772
+ diskCandidates: diskPaths,
773
+ warning
774
+ });
775
+ }
776
+ if (envHit) {
777
+ assertUsableCredential(name, envHit.key, envHit.value);
778
+ return sealCredential({
779
+ apiKey: envHit.value,
780
+ tier: "env",
781
+ source: envHit.key,
782
+ deliberate: false,
783
+ diskCandidates: diskPaths,
784
+ warning: null
785
+ });
786
+ }
787
+ return null;
788
+ }
789
+ var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
790
+ var requireSecretsSdk = createRequire(import.meta.url);
791
+ var DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com";
792
+ var DEFAULT_AUTHORITY_SOURCE = "default";
793
+ function defaultFleetGatewayBaseUrl(name) {
794
+ return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
795
+ }
796
+ var ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
797
+ var DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
798
+ function isValidDnsDomain(value) {
799
+ if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN.test(value) || /[^\x00-\x7f]/.test(value)) {
800
+ return false;
801
+ }
802
+ return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
803
+ }
804
+ function validateAppSlug(name) {
805
+ if (name.length > 63 || !DNS_LABEL_PATTERN.test(name)) {
806
+ throw new Error("App name must be one lowercase DNS label.");
807
+ }
808
+ return name;
809
+ }
810
+ function rawAuthority(value) {
811
+ const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
812
+ if (!match)
813
+ throw new Error("API URL must be absolute.");
814
+ const afterScheme = value.slice(match[0].length);
815
+ const boundary = afterScheme.search(/[/?#]/);
816
+ const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
817
+ if (!authority)
818
+ throw new Error("API URL must include a hostname.");
819
+ return authority;
820
+ }
821
+ function assertCanonicalPort(port) {
822
+ if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
823
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
824
+ }
825
+ const numericPort = Number(port);
826
+ if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
827
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
828
+ }
829
+ }
830
+ function canonicalAuthorityHostname(authority) {
831
+ let rawHostname;
832
+ if (authority.startsWith("[")) {
833
+ const closingBracket = authority.indexOf("]");
834
+ if (closingBracket === -1) {
835
+ throw new Error("API URL authority must contain a canonical hostname.");
836
+ }
837
+ rawHostname = authority.slice(0, closingBracket + 1);
838
+ const portSuffix = authority.slice(closingBracket + 1);
839
+ if (portSuffix) {
840
+ if (!portSuffix.startsWith(":")) {
841
+ throw new Error("API URL authority must contain a canonical hostname and port.");
842
+ }
843
+ assertCanonicalPort(portSuffix.slice(1));
844
+ }
845
+ if (isIP(rawHostname.slice(1, -1)) !== 6) {
846
+ throw new Error("API URL authority must contain a canonical IPv6 literal.");
847
+ }
848
+ } else {
849
+ const firstColon = authority.indexOf(":");
850
+ const lastColon = authority.lastIndexOf(":");
851
+ if (firstColon !== lastColon) {
852
+ throw new Error("IPv6 API URL authorities must use brackets.");
853
+ }
854
+ if (lastColon !== -1) {
855
+ const port = authority.slice(lastColon + 1);
856
+ assertCanonicalPort(port);
857
+ rawHostname = authority.slice(0, lastColon);
858
+ } else {
859
+ rawHostname = authority;
860
+ }
861
+ const ipVersion = isIP(rawHostname);
862
+ const numericAddressParts = rawHostname.split(".");
863
+ const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
864
+ if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
865
+ throw new Error("API URL authority must contain a canonical ASCII hostname.");
866
+ }
867
+ }
868
+ return rawHostname.toLowerCase();
869
+ }
870
+ function isDeliberateLoopbackHttpAuthority(authority) {
871
+ return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
872
+ }
873
+ function toV1BaseUrl(apiUrl) {
874
+ if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
875
+ throw new Error("API URL must not contain ASCII control characters.");
876
+ }
877
+ const input = apiUrl.trim();
878
+ const authority = rawAuthority(input);
879
+ if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
880
+ throw new Error("API URL authority must be canonical ASCII without credentials.");
881
+ }
882
+ const canonicalHostname = canonicalAuthorityHostname(authority);
883
+ const url = new URL(input);
884
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
885
+ throw new Error("API URL must use http or https.");
886
+ }
887
+ if (url.username || url.password) {
888
+ throw new Error("API URL must not include credentials.");
889
+ }
890
+ if (!url.hostname || url.hostname.endsWith(".")) {
891
+ throw new Error("API URL must include a canonical hostname.");
892
+ }
893
+ if (url.hostname.toLowerCase() !== canonicalHostname) {
894
+ throw new Error("API URL authority must not rely on parser hostname normalization.");
895
+ }
896
+ if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
897
+ throw new Error("API URL must not use IDN or punycode hostnames.");
898
+ }
899
+ if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
900
+ throw new Error("API URL may use http only for an exact loopback authority.");
901
+ }
902
+ if (url.search || url.hash) {
903
+ throw new Error("API URL must not include a query string or fragment.");
904
+ }
905
+ let path = url.pathname.replace(/\/+$/, "");
906
+ if (path.endsWith("/v1"))
907
+ path = path.slice(0, -"/v1".length);
908
+ url.pathname = `${path}/v1`;
909
+ return url.toString().replace(/\/+$/, "");
910
+ }
911
+ class ClientTransportConfigurationError extends Error {
912
+ appName;
913
+ sources;
914
+ constructor(appName, message, sources = []) {
915
+ super(message);
916
+ this.name = "ClientTransportConfigurationError";
917
+ this.appName = appName;
918
+ this.sources = Object.freeze([...sources]);
919
+ }
920
+ }
921
+ function resolveClientTransportSnapshot(name, env = process.env, options = {}) {
922
+ env = snapshotClientEnvironment(name, env);
923
+ const keys = clientTransportEnvKeys(name);
924
+ const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
925
+ const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
926
+ if (blankUrl) {
927
+ throw new ClientTransportConfigurationError(name, `${blankUrl.key} is set but blank; public clients require an explicit HTTPS API URL and never select local storage.`, [blankUrl.key]);
928
+ }
929
+ const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
930
+ if (controlledUrl) {
931
+ throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
932
+ }
933
+ const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
934
+ if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
935
+ throw new ClientTransportConfigurationError(name, `${usableUrlEntries.map((entry) => entry.key).join(" and ")} disagree; client authority aliases must be identical or only one may be set.`, usableUrlEntries.map((entry) => entry.key));
936
+ }
937
+ const envUrlHit = usableUrlEntries[0] ?? null;
938
+ const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
939
+ const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys);
940
+ if (diskConfigUrlHit?.unusable) {
941
+ throw new ClientTransportConfigurationError(name, `${diskConfigUrlHit.key} in ${diskConfigUrlHit.path} is declared but blank or malformed; public clients require a valid HTTPS service authority.`, [diskConfigUrlHit.path]);
942
+ }
943
+ const urlCandidates = [
944
+ ...envUrlHit ? [envUrlHit] : [],
945
+ ...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
946
+ ...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
947
+ ];
948
+ const configuredUrl = urlCandidates[0] ?? null;
949
+ const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
950
+ if (configuredUrl && divergentUrls.length > 0) {
951
+ throw new ClientTransportConfigurationError(name, `${configuredUrl.key} and ${divergentUrls.map((candidate) => candidate.key).join(" and ")} select different service authorities; refusing to send a credential written for one authority to the other.`, urlCandidates.map((candidate) => candidate.key));
952
+ }
953
+ const warnings = [];
954
+ if (configuredUrl && !envUrlHit) {
955
+ warnings.push(`No ${keys.apiUrlKeys[0]} in the environment; the server URL in ${configuredUrl.key} was used, so this client connects to the server. ` + `Keep that entry aligned with the intended service authority.`);
956
+ }
957
+ const credential = resolveCredential(name, env, options.credentials);
958
+ if (!credential) {
959
+ const diskHint = credentialDiskSourcesForMessage(name, env);
960
+ const lead = configuredUrl ? `${configuredUrl.key} selects the HTTP server for '${name}', but no API key could be resolved` : `${keys.apiUrlKeys[0]} is not set and no API key could be resolved for '${name}'; a credential is required before the default fleet gateway authority applies`;
961
+ warnings.push(`${lead}; refusing to create an unauthenticated client \u2014 public clients never fall back to SQLite or another local store. ` + `Looked in the Keychain (macOS only), then for a credential file at ${diskHint}, then for ${keys.apiKeyKeys[0]} in the environment.`);
962
+ throw new ClientTransportConfigurationError(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
963
+ }
964
+ if (credential.warning)
965
+ warnings.push(credential.warning);
966
+ let urlHit;
967
+ if (configuredUrl) {
968
+ urlHit = configuredUrl;
969
+ } else {
970
+ try {
971
+ urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
972
+ } catch (error) {
973
+ const message = error instanceof Error ? error.message : String(error);
974
+ throw new ClientTransportConfigurationError(name, `No ${keys.apiUrlKeys[0]} is configured and the default fleet gateway authority cannot be composed for '${name}': ${message}`, [keys.apiUrlKeys[0]]);
975
+ }
976
+ }
977
+ const apiUrlSource = urlHit.key;
978
+ let baseUrl;
979
+ try {
980
+ baseUrl = toV1BaseUrl(urlHit.value);
981
+ } catch (error) {
982
+ const message = error instanceof Error ? error.message : String(error);
983
+ throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
984
+ }
985
+ return {
986
+ resolution: {
987
+ transport: "http",
988
+ transportSource: urlHit.key,
989
+ baseUrl,
990
+ apiUrlSource,
991
+ apiKeyPresent: true,
992
+ apiKeySource: credential.source,
993
+ apiKeyTier: credential.tier,
994
+ misconfigured: false,
995
+ warning: warnings.length > 0 ? warnings.join(" ") : null
996
+ },
997
+ credential
998
+ };
999
+ }
1000
+ function resolveClientTransport(name, env = process.env, options = {}) {
1001
+ return resolveClientTransportSnapshot(name, env, options).resolution;
1002
+ }
1003
+ function credentialDiskSourcesForMessage(name, env) {
1004
+ const paths = credentialDiskSources(name, env);
1005
+ return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
1006
+ }
1007
+ var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
1008
+ var AUTHORITY_OVERRIDE_HEADERS = new Set([
1009
+ "host",
1010
+ ":authority",
1011
+ "forwarded",
1012
+ "x-forwarded-host",
1013
+ "x-original-host"
1014
+ ]);
1015
+
1016
+ // src/lib/local-opt-in.ts
1017
+ var MEMENTOS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_MEMENTOS_LOCAL", "MEMENTOS_LOCAL"];
1018
+ var MEMENTOS_DB_PATH_ENV_KEYS = ["HASNA_MEMENTOS_DB_PATH", "MEMENTOS_DB_PATH"];
1019
+ function isMementosLocalOptIn(env = process.env) {
1020
+ return MEMENTOS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env[key] ?? "").trim() !== "");
1021
+ }
1022
+ function hasExplicitLocalDbPath(env = process.env) {
1023
+ return MEMENTOS_DB_PATH_ENV_KEYS.some((key) => (env[key] ?? "").trim() !== "");
1024
+ }
1025
+ function mementosAuthorityEnvKeys() {
1026
+ const keys = clientTransportEnvKeys("mementos");
1027
+ return [
1028
+ ...keys.apiUrlKeys,
1029
+ ...keys.apiKeyKeys,
1030
+ credentialOverrideEnvKey("mementos"),
1031
+ credentialPointerEnvKey("mementos"),
1032
+ CREDENTIAL_PROFILE_ENV_KEY
1033
+ ];
1034
+ }
1035
+ function hasMementosEnvAuthorityIntent(env = process.env) {
1036
+ return mementosAuthorityEnvKeys().some((key) => (env[key] ?? "").trim() !== "");
1037
+ }
1038
+ function selectsMementosLocalStore(env = process.env) {
1039
+ if (hasExplicitLocalDbPath(env))
1040
+ return true;
1041
+ return !hasMementosEnvAuthorityIntent(env) && isMementosLocalOptIn(env);
1042
+ }
1043
+ function mementosResolverEnv(env) {
1044
+ const blanks = mementosAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
1045
+ if (blanks.length === 0)
1046
+ return env;
1047
+ const next = { ...env };
1048
+ for (const key of blanks)
1049
+ delete next[key];
1050
+ return next;
1051
+ }
1052
+ var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
1053
+ function isAmbientMementosEnv(env) {
1054
+ if (typeof process !== "undefined" && env === process.env)
1055
+ return true;
1056
+ return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
1057
+ }
1058
+ function mementosResolverInputs(env, credentials = {}) {
1059
+ const normalised = mementosResolverEnv(env);
1060
+ if (normalised === env)
1061
+ return { env: normalised, credentials };
1062
+ const keychain = { ...credentials.keychain };
1063
+ if (keychain.enabled === undefined && keychain.run === undefined) {
1064
+ keychain.enabled = isAmbientMementosEnv(env);
1065
+ }
1066
+ return { env: normalised, credentials: { ...credentials, keychain } };
1067
+ }
1068
+
2
1069
  // src/sdk/index.ts
1070
+ var MEMENTOS_MACHINE_REGISTRATION_CONTRACT = "mementos.machine-registration.v1";
1071
+ var MEMENTOS_MACHINE_LIST_CONTRACT = "mementos.machines.v1";
1072
+ var MEMENTOS_MACHINE_MUTATION_CONTRACT = "mementos.machine-mutation.v1";
1073
+ var MEMENTOS_MACHINE_TOUCH_CONTRACT = "mementos.machine-touch.v1";
1074
+
3
1075
  class MementosError extends Error {
4
1076
  status;
5
1077
  details;
@@ -11,26 +1083,449 @@ class MementosError extends Error {
11
1083
  }
12
1084
  }
13
1085
 
1086
+ class MementosConfigError extends Error {
1087
+ code = "MEMENTOS_STORE_CONFIG";
1088
+ constructor(message, options) {
1089
+ super(message, options);
1090
+ this.name = "MementosConfigError";
1091
+ }
1092
+ }
1093
+ var SESSION_JOBS_PAGE_CONTRACT = "mementos.sessions.jobs.v2";
1094
+ function sdkProtocolError(operation, detail) {
1095
+ return new MementosError(`mementos ${operation} returned a malformed 2xx response: ${detail}`, 502);
1096
+ }
1097
+ function sessionProtocolError(detail) {
1098
+ return sdkProtocolError("session jobs", detail);
1099
+ }
1100
+ function auditSdkError(operation, error) {
1101
+ if (error instanceof MementosError)
1102
+ throw error;
1103
+ const detail = error instanceof Error ? error.message : String(error);
1104
+ throw sdkProtocolError(operation, detail);
1105
+ }
1106
+ function sdkAuditIdentifier(value, label) {
1107
+ if (value === undefined)
1108
+ return null;
1109
+ if (!value || value.length > 512 || /[\u0000-\u001f\u007f]/.test(value)) {
1110
+ throw new AuditContractError(`${label} must be a non-empty printable string of at most 512 characters`);
1111
+ }
1112
+ return value;
1113
+ }
1114
+ function sdkAuditFilters(input) {
1115
+ const filters = {
1116
+ memory_id: sdkAuditIdentifier(input.memory_id, "memory_id"),
1117
+ since: input.since === undefined ? null : canonicalAuditTimestamp(input.since, "since"),
1118
+ until: input.until === undefined ? null : canonicalAuditTimestamp(input.until, "until"),
1119
+ operation: input.operation === undefined ? null : auditOperation(input.operation),
1120
+ agent_id: sdkAuditIdentifier(input.agent_id, "agent_id")
1121
+ };
1122
+ if (filters.since && filters.until && filters.since > filters.until) {
1123
+ throw new AuditContractError("since must not be after until");
1124
+ }
1125
+ return filters;
1126
+ }
1127
+ function sdkAuditCursor(cursor) {
1128
+ if (cursor === undefined)
1129
+ return;
1130
+ if (!cursor || cursor.length > 4096 || !/^[A-Za-z0-9_-]+$/.test(cursor)) {
1131
+ throw new AuditContractError("cursor must be a bounded base64url audit cursor");
1132
+ }
1133
+ return cursor;
1134
+ }
1135
+ function sdkAuditLimit(limit) {
1136
+ if (limit === undefined)
1137
+ return;
1138
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1000) {
1139
+ throw new AuditContractError("limit must be an integer between 1 and 1000");
1140
+ }
1141
+ return limit;
1142
+ }
1143
+ function sdkObject(value, operation, field = "response") {
1144
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1145
+ throw sdkProtocolError(operation, `expected ${field} to be a JSON object`);
1146
+ }
1147
+ return value;
1148
+ }
1149
+ var SDK_MACHINE_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
1150
+ function sdkMachineHostname(value, operation) {
1151
+ const normalized = value.trim().replace(/\.+$/, "").toLowerCase();
1152
+ if (!normalized || normalized.length > 253 || /[\u0000-\u001f\u007f/\\\s]/.test(normalized) || normalized !== value) {
1153
+ throw sdkProtocolError(operation, "machine.hostname is not canonical");
1154
+ }
1155
+ return normalized;
1156
+ }
1157
+ function sdkMachinePlatform(value, operation) {
1158
+ const normalized = value.trim().toLowerCase();
1159
+ if (!normalized || normalized.length > 64 || !/^[a-z0-9._-]+$/.test(normalized) || normalized !== value) {
1160
+ throw sdkProtocolError(operation, "machine.platform is not canonical");
1161
+ }
1162
+ return normalized;
1163
+ }
1164
+ function sdkMachineName(value, operation) {
1165
+ if (typeof value !== "string" || !value || value.length > 128 || /[\u0000-\u001f\u007f]/.test(value)) {
1166
+ throw sdkProtocolError(operation, "machine.name is outside the public contract");
1167
+ }
1168
+ return value;
1169
+ }
1170
+ function sdkMachineTimestamp(value, operation, field) {
1171
+ if (typeof value !== "string" || !SDK_MACHINE_TIMESTAMP.test(value)) {
1172
+ throw sdkProtocolError(operation, `machine.${field} is not a canonical UTC timestamp`);
1173
+ }
1174
+ const parsed = new Date(value);
1175
+ if (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== value) {
1176
+ throw sdkProtocolError(operation, `machine.${field} is not a real calendar timestamp`);
1177
+ }
1178
+ return value;
1179
+ }
1180
+ function sdkMachine(value, operation) {
1181
+ const machine = sdkObject(value, operation, "machine");
1182
+ if (typeof machine["id"] !== "string" || !machine["id"]) {
1183
+ throw sdkProtocolError(operation, "expected machine.id to be a non-empty string");
1184
+ }
1185
+ if (typeof machine["hostname"] !== "string" || typeof machine["platform"] !== "string") {
1186
+ throw sdkProtocolError(operation, "expected machine hostname/platform strings");
1187
+ }
1188
+ if (typeof machine["is_primary"] !== "boolean") {
1189
+ throw sdkProtocolError(operation, "expected machine.is_primary to be a boolean");
1190
+ }
1191
+ const createdAt = sdkMachineTimestamp(machine["created_at"], operation, "created_at");
1192
+ const lastSeenAt = sdkMachineTimestamp(machine["last_seen_at"], operation, "last_seen_at");
1193
+ if (lastSeenAt < createdAt)
1194
+ throw sdkProtocolError(operation, "machine.last_seen_at precedes created_at");
1195
+ return {
1196
+ id: machine["id"],
1197
+ name: sdkMachineName(machine["name"], operation),
1198
+ hostname: sdkMachineHostname(machine["hostname"], operation),
1199
+ platform: sdkMachinePlatform(machine["platform"], operation),
1200
+ is_primary: machine["is_primary"],
1201
+ created_at: createdAt,
1202
+ last_seen_at: lastSeenAt
1203
+ };
1204
+ }
1205
+ function sdkMachineMutation(value, operation, expectedId) {
1206
+ const response = sdkObject(value, operation);
1207
+ if (response["contract"] !== MEMENTOS_MACHINE_MUTATION_CONTRACT) {
1208
+ throw sdkProtocolError(operation, `expected contract '${MEMENTOS_MACHINE_MUTATION_CONTRACT}'`);
1209
+ }
1210
+ const machine = sdkMachine(response["machine"], operation);
1211
+ if (machine.id !== expectedId) {
1212
+ throw sdkProtocolError(operation, "returned machine id does not match the requested stable id");
1213
+ }
1214
+ return machine;
1215
+ }
1216
+ function sessionObject(value, field = "response") {
1217
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1218
+ throw sessionProtocolError(`expected ${field} to be a JSON object`);
1219
+ }
1220
+ return value;
1221
+ }
1222
+ function sessionString(object2, field, nullable = false) {
1223
+ const value = object2[field];
1224
+ if (nullable && value === null)
1225
+ return null;
1226
+ if (typeof value !== "string" || !nullable && value.length === 0) {
1227
+ throw sessionProtocolError(`expected '${field}' to be ${nullable ? "a string or null" : "a non-empty string"}`);
1228
+ }
1229
+ return value;
1230
+ }
1231
+ function sessionCount(object2, field) {
1232
+ const value = object2[field];
1233
+ if (!Number.isSafeInteger(value) || value < 0) {
1234
+ throw sessionProtocolError(`expected '${field}' to be a non-negative safe integer`);
1235
+ }
1236
+ return value;
1237
+ }
1238
+ function decodeSessionJob(value, index) {
1239
+ const job = sessionObject(value, `jobs[${index}]`);
1240
+ const source = sessionString(job, "source");
1241
+ if (!["claude-code", "codex", "manual", "open-sessions"].includes(source)) {
1242
+ throw sessionProtocolError(`jobs[${index}] has unsupported 'source'`);
1243
+ }
1244
+ const status = sessionString(job, "status");
1245
+ if (!["pending", "processing", "completed", "failed"].includes(status)) {
1246
+ throw sessionProtocolError(`jobs[${index}] has unsupported 'status'`);
1247
+ }
1248
+ const metadata = sessionObject(job["metadata"], `jobs[${index}].metadata`);
1249
+ return {
1250
+ id: sessionString(job, "id"),
1251
+ session_id: sessionString(job, "session_id"),
1252
+ agent_id: sessionString(job, "agent_id", true),
1253
+ project_id: sessionString(job, "project_id", true),
1254
+ source,
1255
+ status,
1256
+ transcript: typeof job["transcript"] === "string" ? job["transcript"] : (() => {
1257
+ throw sessionProtocolError(`jobs[${index}] expected 'transcript' to be a string`);
1258
+ })(),
1259
+ chunk_count: sessionCount(job, "chunk_count"),
1260
+ memories_extracted: sessionCount(job, "memories_extracted"),
1261
+ error: sessionString(job, "error", true),
1262
+ metadata,
1263
+ created_at: sessionString(job, "created_at"),
1264
+ started_at: sessionString(job, "started_at", true),
1265
+ completed_at: sessionString(job, "completed_at", true)
1266
+ };
1267
+ }
1268
+ function decodeSessionJobsPage(value) {
1269
+ const page = sessionObject(value);
1270
+ if (page["contract"] !== SESSION_JOBS_PAGE_CONTRACT) {
1271
+ throw sessionProtocolError(`expected contract '${SESSION_JOBS_PAGE_CONTRACT}'`);
1272
+ }
1273
+ if (!Array.isArray(page["jobs"]))
1274
+ throw sessionProtocolError("expected 'jobs' to be an array");
1275
+ const jobs = page["jobs"].map(decodeSessionJob);
1276
+ const count = sessionCount(page, "count");
1277
+ const limit = sessionCount(page, "limit");
1278
+ const offset = sessionCount(page, "offset");
1279
+ if (limit < 1)
1280
+ throw sessionProtocolError("expected 'limit' to be positive");
1281
+ if (count !== jobs.length || count > limit) {
1282
+ throw sessionProtocolError("page count is inconsistent with jobs/limit");
1283
+ }
1284
+ if (typeof page["has_more"] !== "boolean")
1285
+ throw sessionProtocolError("expected 'has_more' to be a boolean");
1286
+ const nextOffset = page["next_offset"];
1287
+ if (nextOffset !== null && (!Number.isSafeInteger(nextOffset) || nextOffset < 0)) {
1288
+ throw sessionProtocolError("expected 'next_offset' to be a non-negative safe integer or null");
1289
+ }
1290
+ if (page["has_more"] === true && nextOffset !== offset + jobs.length) {
1291
+ throw sessionProtocolError("'has_more' requires the exact next offset");
1292
+ }
1293
+ if (page["has_more"] === false && nextOffset !== null) {
1294
+ throw sessionProtocolError("terminal page must set 'next_offset' to null");
1295
+ }
1296
+ return {
1297
+ contract: SESSION_JOBS_PAGE_CONTRACT,
1298
+ jobs,
1299
+ count,
1300
+ limit,
1301
+ offset,
1302
+ has_more: page["has_more"],
1303
+ next_offset: nextOffset
1304
+ };
1305
+ }
1306
+ function decodeSessionIngestReceipt(value, transcript, expectedSessionId) {
1307
+ const receipt = sessionObject(value);
1308
+ if (receipt["contract"] !== "mementos.sessions.ingest.v2") {
1309
+ throw sdkProtocolError("session ingest", "expected contract 'mementos.sessions.ingest.v2'");
1310
+ }
1311
+ const jobId = sessionString(receipt, "job_id");
1312
+ if (receipt["status"] !== "queued")
1313
+ throw sdkProtocolError("session ingest", "expected status 'queued'");
1314
+ const message = sessionString(receipt, "message");
1315
+ const rawJob = sessionObject(receipt["job"], "job");
1316
+ const job = decodeSessionJob({ ...rawJob, transcript }, 0);
1317
+ if (job.id !== jobId || job.session_id !== expectedSessionId) {
1318
+ throw sdkProtocolError("session ingest", "job receipt identity does not match the request");
1319
+ }
1320
+ return {
1321
+ contract: "mementos.sessions.ingest.v2",
1322
+ job_id: jobId,
1323
+ status: "queued",
1324
+ message,
1325
+ job
1326
+ };
1327
+ }
1328
+ function decodeQueueStats(value) {
1329
+ const stats = sessionObject(value);
1330
+ return {
1331
+ pending: sessionCount(stats, "pending"),
1332
+ processing: sessionCount(stats, "processing"),
1333
+ completed: sessionCount(stats, "completed"),
1334
+ failed: sessionCount(stats, "failed")
1335
+ };
1336
+ }
1337
+ var SDK_RESOURCE_TYPES = new Set(["project", "memory", "entity", "agent", "connector", "file"]);
1338
+ var SDK_LOCK_TYPES = new Set(["advisory", "exclusive"]);
1339
+ function decodeResourceLock(value, operation) {
1340
+ const lock = sessionObject(value, "lock");
1341
+ const resourceType = sessionString(lock, "resource_type");
1342
+ const lockType = sessionString(lock, "lock_type");
1343
+ if (!SDK_RESOURCE_TYPES.has(resourceType)) {
1344
+ throw sdkProtocolError(operation, "unsupported 'resource_type'");
1345
+ }
1346
+ if (!SDK_LOCK_TYPES.has(lockType)) {
1347
+ throw sdkProtocolError(operation, "unsupported 'lock_type'");
1348
+ }
1349
+ return {
1350
+ id: sessionString(lock, "id"),
1351
+ resource_type: resourceType,
1352
+ resource_id: sessionString(lock, "resource_id"),
1353
+ agent_id: sessionString(lock, "agent_id"),
1354
+ lock_type: lockType,
1355
+ locked_at: sessionString(lock, "locked_at"),
1356
+ expires_at: sessionString(lock, "expires_at")
1357
+ };
1358
+ }
1359
+ function decodeResourceLocks(value, operation) {
1360
+ if (!Array.isArray(value))
1361
+ throw sdkProtocolError(operation, "expected a JSON array");
1362
+ return value.map((lock) => decodeResourceLock(lock, operation));
1363
+ }
1364
+ function decodeBooleanReceipt(value, field, operation) {
1365
+ const receipt = sessionObject(value);
1366
+ if (typeof receipt[field] !== "boolean")
1367
+ throw sdkProtocolError(operation, `expected '${field}' to be a boolean`);
1368
+ return { [field]: receipt[field] };
1369
+ }
1370
+ function decodeCountReceipt(value, field, operation) {
1371
+ const receipt = sessionObject(value);
1372
+ const count = receipt[field];
1373
+ if (!Number.isSafeInteger(count) || count < 0) {
1374
+ throw sdkProtocolError(operation, `expected '${field}' to be a non-negative safe integer`);
1375
+ }
1376
+ return { [field]: count };
1377
+ }
1378
+ var MEMENTOS_DEFAULT_BASE_URL = "http://localhost:19428";
1379
+ function resolveMementosApiBase(rawBaseUrl, explicitPrefix) {
1380
+ const trimmed = (rawBaseUrl ?? MEMENTOS_DEFAULT_BASE_URL).trim().replace(/\/+$/, "") || MEMENTOS_DEFAULT_BASE_URL;
1381
+ let url;
1382
+ try {
1383
+ url = new URL(trimmed);
1384
+ } catch {
1385
+ throw new Error("mementos base URL must be an absolute http(s) URL (the configured value does not parse as a URL)");
1386
+ }
1387
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
1388
+ throw new Error("mementos base URL must be an absolute http(s) URL");
1389
+ }
1390
+ if (url.username || url.password || url.search || url.hash || /[?#]/.test(trimmed)) {
1391
+ throw new Error("mementos base URL must not contain userinfo, query, or fragment data");
1392
+ }
1393
+ const prefixMatch = /^(.*)(\/(?:v1|api))$/.exec(trimmed);
1394
+ if (explicitPrefix !== undefined) {
1395
+ const prefix = explicitPrefix.replace(/\/+$/, "");
1396
+ const baseUrl = prefixMatch ? prefixMatch[1] || trimmed : trimmed;
1397
+ return { baseUrl, prefix };
1398
+ }
1399
+ if (prefixMatch)
1400
+ return { baseUrl: prefixMatch[1] || trimmed, prefix: prefixMatch[2] };
1401
+ return { baseUrl: trimmed, prefix: "/v1" };
1402
+ }
1403
+ var localNoticePrinted = false;
1404
+ function __resetMementosSdkLocalNotice() {
1405
+ localNoticePrinted = false;
1406
+ }
1407
+ function stripV1(baseUrl) {
1408
+ return baseUrl.replace(/\/+$/, "").replace(/\/v1$/, "");
1409
+ }
1410
+ function assertCleanSdkBase(baseUrl) {
1411
+ if (/[?#]/.test(baseUrl)) {
1412
+ throw new Error("mementos base URL must not contain userinfo, query, or fragment data");
1413
+ }
1414
+ return baseUrl;
1415
+ }
1416
+ function announceLocal(notice, reason) {
1417
+ if (localNoticePrinted)
1418
+ return;
1419
+ localNoticePrinted = true;
1420
+ const line = `mementos: LOCAL mode \u2014 reading and writing the on-box mementos-serve at ${MEMENTOS_DEFAULT_BASE_URL} ` + `(${reason}), not the hosted fleet. Unset it and set HASNA_MEMENTOS_API_KEY, add the Keychain item ` + `hasna.credentials.mementos.api-key, or write ~/.hasna/mementos/config/credentials to go hosted.`;
1421
+ if (notice)
1422
+ notice(line);
1423
+ else if (typeof process !== "undefined")
1424
+ process.stderr.write(`${line}
1425
+ `);
1426
+ }
1427
+ function unconfiguredSdkMessage() {
1428
+ const keys = clientTransportEnvKeys("mementos");
1429
+ const urlKey = keys.apiUrlKeys[0];
1430
+ const keyKey = keys.apiKeyKeys[0];
1431
+ return "mementos is not configured to reach any memory store, and will NOT fall back to the " + `on-box mementos-serve (${MEMENTOS_DEFAULT_BASE_URL}). ` + "No credential could be resolved from the Keychain item hasna.credentials.mementos.api-key " + `(macOS only), ~/.hasna/mementos/config/credentials, or ${keyKey}; the authority would be the ` + `fleet gateway https://api.hasna.com/mementos (or ${urlKey} if set). ` + `Set ${keyKey} (the resolver also accepts the legacy ${keys.apiKeyKeys[1]} alias for one release), ` + "add the Keychain item, or write the credentials file to use the fleet memory API, or opt into " + `the on-box mementos-serve with ${MEMENTOS_LOCAL_OPT_IN_ENV_KEYS[0]}=1 (or an explicit ` + `${MEMENTOS_DB_PATH_ENV_KEYS[0]}).`;
1432
+ }
1433
+ function resolveMementosSdkTransport(options = {}) {
1434
+ const rawEnv = options.env ?? (typeof process !== "undefined" ? process.env : {});
1435
+ const requestedCredentials = {
1436
+ ...options.credentials,
1437
+ ...options.apiKey !== undefined ? { apiKey: options.apiKey } : {}
1438
+ };
1439
+ const { env, credentials } = mementosResolverInputs(rawEnv, requestedCredentials);
1440
+ if (options.baseUrl) {
1441
+ return {
1442
+ mode: "http",
1443
+ baseUrl: stripV1(options.baseUrl),
1444
+ apiKey: options.apiKey ?? null,
1445
+ apiKeySource: options.apiKey ? "explicit apiKey argument" : null,
1446
+ apiUrlSource: "explicit baseUrl argument"
1447
+ };
1448
+ }
1449
+ if (selectsMementosLocalStore(env)) {
1450
+ announceLocal(options.notice, "HASNA_MEMENTOS_LOCAL is set (or HASNA_MEMENTOS_DB_PATH) and nothing configures an authority");
1451
+ return {
1452
+ mode: "local-serve",
1453
+ baseUrl: MEMENTOS_DEFAULT_BASE_URL,
1454
+ apiKey: options.apiKey ?? null,
1455
+ apiKeySource: options.apiKey ? "explicit apiKey argument" : null,
1456
+ apiUrlSource: "local-serve"
1457
+ };
1458
+ }
1459
+ let credential = null;
1460
+ credential = resolveCredential("mementos", env, credentials);
1461
+ const chainOptions = {
1462
+ credentials: credential ? { ...credentials, apiKey: credential.apiKey } : credentials
1463
+ };
1464
+ let resolution;
1465
+ try {
1466
+ resolution = resolveClientTransport("mementos", env, chainOptions);
1467
+ } catch (error) {
1468
+ if (error instanceof ClientTransportConfigurationError && !credential && /is not set and no API key could be resolved/.test(error.message)) {
1469
+ throw new MementosConfigError(unconfiguredSdkMessage(), { cause: error });
1470
+ }
1471
+ throw error;
1472
+ }
1473
+ return {
1474
+ mode: "http",
1475
+ baseUrl: stripV1(assertCleanSdkBase(resolution.baseUrl)),
1476
+ apiKey: credential ? credential.apiKey : null,
1477
+ apiKeySource: credential ? credential.source : resolution.apiKeySource,
1478
+ apiUrlSource: resolution.apiUrlSource ?? "default"
1479
+ };
1480
+ }
1481
+
14
1482
  class MementosClient {
15
- baseUrl;
16
1483
  _fetch;
17
1484
  apiKey;
18
1485
  prefix;
1486
+ _resolveOptions;
1487
+ _pinnedAuthority = null;
19
1488
  constructor(config = {}) {
20
- this.baseUrl = (config.baseUrl ?? "http://localhost:19428").replace(/\/$/, "");
1489
+ const resolved = resolveMementosApiBase(config.baseUrl, config.prefix);
21
1490
  this._fetch = config.fetch ?? globalThis.fetch.bind(globalThis);
22
1491
  this.apiKey = config.apiKey;
23
- this.prefix = (config.prefix ?? "/v1").replace(/\/$/, "");
1492
+ this.prefix = resolved.prefix;
1493
+ this._resolveOptions = {
1494
+ ...config.baseUrl !== undefined ? { baseUrl: config.baseUrl } : {},
1495
+ ...config.apiKey !== undefined ? { apiKey: config.apiKey } : {},
1496
+ ...config.credentials !== undefined ? { credentials: config.credentials } : {},
1497
+ ...config.env !== undefined ? { env: config.env } : {},
1498
+ ...config.notice !== undefined ? { notice: config.notice } : {}
1499
+ };
24
1500
  }
25
1501
  static fromEnv(overrides = {}) {
26
- const env = typeof process !== "undefined" ? process.env : undefined;
27
- const baseUrl = env?.["MEMENTOS_API_URL"] ?? env?.["MEMENTOS_URL"] ?? "http://localhost:19428";
28
- const apiKey = env?.["MEMENTOS_API_KEY"];
29
- return new MementosClient({ baseUrl, apiKey, ...overrides });
1502
+ return new MementosClient(overrides);
1503
+ }
1504
+ get apiUrl() {
1505
+ const transport = resolveMementosSdkTransport(this._resolveOptions);
1506
+ const base = this.pinnedTarget(transport);
1507
+ return `${base}${this.prefix}`;
1508
+ }
1509
+ currentTransport() {
1510
+ return resolveMementosSdkTransport(this._resolveOptions);
1511
+ }
1512
+ targetOf(transport) {
1513
+ return transport.mode === "http" ? transport.baseUrl : MEMENTOS_DEFAULT_BASE_URL;
1514
+ }
1515
+ pinnedTarget(transport) {
1516
+ const target = this.targetOf(transport);
1517
+ if (this._pinnedAuthority !== null && target !== this._pinnedAuthority) {
1518
+ throw new MementosConfigError(`MEMENTOS_AUTHORITY_CHANGED: the configured service authority changed from ${this._pinnedAuthority} to ${target}. ` + "A credential is only ever sent to the authority it resolved with \u2014 construct a new client " + "before sending data (hasna/apps#1794).");
1519
+ }
1520
+ this._pinnedAuthority = target;
1521
+ return target;
30
1522
  }
31
1523
  async request(method, path, body, query) {
1524
+ const transport = this.currentTransport();
1525
+ const baseUrl = this.pinnedTarget(transport);
1526
+ const apiKey = transport.mode === "http" ? transport.apiKey ?? this.apiKey : this.apiKey;
32
1527
  const routed = path.startsWith("/api/") ? `${this.prefix}${path.slice(4)}` : path;
33
- let url = `${this.baseUrl}${routed}`;
1528
+ let url = `${baseUrl}${routed}`;
34
1529
  if (query) {
35
1530
  const params = new URLSearchParams;
36
1531
  for (const [k, v] of Object.entries(query)) {
@@ -44,9 +1539,9 @@ class MementosClient {
44
1539
  const headers = {};
45
1540
  if (body !== undefined)
46
1541
  headers["Content-Type"] = "application/json";
47
- if (this.apiKey) {
48
- headers["Authorization"] = `Bearer ${this.apiKey}`;
49
- headers["x-api-key"] = this.apiKey;
1542
+ if (apiKey) {
1543
+ headers["Authorization"] = `Bearer ${apiKey}`;
1544
+ headers["x-api-key"] = apiKey;
50
1545
  }
51
1546
  const res = await this._fetch(url, {
52
1547
  method,
@@ -108,6 +1603,8 @@ class MementosClient {
108
1603
  q["agent_id"] = filter.agent_id;
109
1604
  if (filter.project_id)
110
1605
  q["project_id"] = filter.project_id;
1606
+ if (filter.project_id && filter.include_unassigned_project)
1607
+ q["include_unassigned_project"] = true;
111
1608
  if (filter.session_id)
112
1609
  q["session_id"] = filter.session_id;
113
1610
  if (filter.namespace)
@@ -266,6 +1763,126 @@ class MementosClient {
266
1763
  listAgentsByProject(projectId) {
267
1764
  return this.get(`/api/agents`, { project_id: projectId });
268
1765
  }
1766
+ async getMemoryAuditTrail(memoryId, options = {}) {
1767
+ const operation = "GET /v1/memories/:id/audit-trail";
1768
+ try {
1769
+ const filters = sdkAuditFilters({ memory_id: memoryId });
1770
+ const requestedLimit = sdkAuditLimit(options.limit);
1771
+ const limit = requestedLimit ?? AUDIT_DEFAULT_LIMIT;
1772
+ const requestedCursor = sdkAuditCursor(options.cursor);
1773
+ const cursor = requestedCursor ?? null;
1774
+ const response = await this.get(`/api/memories/${encodeURIComponent(filters.memory_id)}/audit-trail`, { limit: requestedLimit, cursor: requestedCursor });
1775
+ return validateAuditPage(response, { contract: AUDIT_TRAIL_CONTRACT, cursor, filters, limit });
1776
+ } catch (error) {
1777
+ return auditSdkError(operation, error);
1778
+ }
1779
+ }
1780
+ async exportAuditLog(options = {}) {
1781
+ const operation = "GET /v1/audit/export";
1782
+ try {
1783
+ const filters = sdkAuditFilters(options);
1784
+ const requestedLimit = sdkAuditLimit(options.limit);
1785
+ const limit = requestedLimit ?? AUDIT_DEFAULT_LIMIT;
1786
+ const requestedCursor = sdkAuditCursor(options.cursor);
1787
+ const cursor = requestedCursor ?? null;
1788
+ const response = await this.get("/api/audit/export", {
1789
+ since: filters.since ?? undefined,
1790
+ until: filters.until ?? undefined,
1791
+ operation: filters.operation ?? undefined,
1792
+ agent_id: filters.agent_id ?? undefined,
1793
+ limit: requestedLimit,
1794
+ cursor: requestedCursor
1795
+ });
1796
+ return validateAuditPage(response, { contract: AUDIT_EXPORT_CONTRACT, cursor, filters, limit });
1797
+ } catch (error) {
1798
+ return auditSdkError(operation, error);
1799
+ }
1800
+ }
1801
+ async getAuditStats() {
1802
+ const operation = "GET /v1/audit/stats";
1803
+ try {
1804
+ return validateAuditStats(await this.get("/api/audit/stats"));
1805
+ } catch (error) {
1806
+ return auditSdkError(operation, error);
1807
+ }
1808
+ }
1809
+ async listMachines() {
1810
+ const operation = "GET /v1/machines";
1811
+ const response = sdkObject(await this.get("/api/machines"), operation);
1812
+ if (response["contract"] !== MEMENTOS_MACHINE_LIST_CONTRACT || response["complete"] !== true || !Array.isArray(response["machines"])) {
1813
+ throw sdkProtocolError(operation, `expected contract '${MEMENTOS_MACHINE_LIST_CONTRACT}', complete=true, and a machines array`);
1814
+ }
1815
+ const machines = response["machines"].map((entry, index) => sdkMachine(entry, `${operation} machines[${index}]`));
1816
+ if (!Number.isSafeInteger(response["count"]) || response["count"] !== machines.length) {
1817
+ throw sdkProtocolError(operation, "count does not match machines.length");
1818
+ }
1819
+ if (new Set(machines.map((machine) => machine.id)).size !== machines.length || new Set(machines.map((machine) => machine.hostname)).size !== machines.length || new Set(machines.map((machine) => machine.name)).size !== machines.length) {
1820
+ throw sdkProtocolError(operation, "machine ids, hostnames, or names are duplicated");
1821
+ }
1822
+ if (machines.filter((machine) => machine.is_primary).length > 1) {
1823
+ throw sdkProtocolError(operation, "more than one machine is primary");
1824
+ }
1825
+ return { machines, count: machines.length, complete: true };
1826
+ }
1827
+ async registerMachine(input) {
1828
+ const operation = "POST /v1/machines";
1829
+ const response = sdkObject(await this.post("/api/machines", input), operation);
1830
+ if (response["contract"] !== MEMENTOS_MACHINE_REGISTRATION_CONTRACT || typeof response["created"] !== "boolean") {
1831
+ throw sdkProtocolError(operation, `expected contract '${MEMENTOS_MACHINE_REGISTRATION_CONTRACT}' and a boolean created receipt`);
1832
+ }
1833
+ const machine = sdkMachine(response["machine"], operation);
1834
+ const identity = sdkObject(response["identity"], operation, "identity");
1835
+ if (identity["idempotency_key"] !== "normalized_hostname" || identity["stable_id"] !== machine.id) {
1836
+ throw sdkProtocolError(operation, "identity receipt does not match the stable machine id");
1837
+ }
1838
+ const expectedHostname = sdkMachineHostname(input.hostname.trim().replace(/\.+$/, "").toLowerCase(), operation);
1839
+ const expectedPlatform = sdkMachinePlatform(input.platform.trim().toLowerCase(), operation);
1840
+ if (machine.hostname !== expectedHostname || machine.platform !== expectedPlatform) {
1841
+ throw sdkProtocolError(operation, "registration receipt does not match the requested hostname/platform");
1842
+ }
1843
+ if (response["created"] === true) {
1844
+ const expectedName = input.name?.trim() || expectedHostname;
1845
+ if (machine.name !== expectedName)
1846
+ throw sdkProtocolError(operation, "created registration receipt does not match the requested name");
1847
+ }
1848
+ return machine;
1849
+ }
1850
+ async getMachine(id) {
1851
+ return sdkMachineMutation(await this.get(`/api/machines/${encodeURIComponent(id)}`), "GET /v1/machines/:id", id);
1852
+ }
1853
+ async renameMachine(id, name) {
1854
+ const normalizedName = name.trim();
1855
+ const machine = sdkMachineMutation(await this.patch(`/api/machines/${encodeURIComponent(id)}`, { name: normalizedName }), "PATCH /v1/machines/:id", id);
1856
+ if (machine.name !== normalizedName)
1857
+ throw sdkProtocolError("PATCH /v1/machines/:id", "returned name does not match the requested rename");
1858
+ return machine;
1859
+ }
1860
+ async setPrimaryMachine(id) {
1861
+ const machine = sdkMachineMutation(await this.post(`/api/machines/${encodeURIComponent(id)}/primary`), "POST /v1/machines/:id/primary", id);
1862
+ if (!machine.is_primary)
1863
+ throw sdkProtocolError("POST /v1/machines/:id/primary", "returned machine is not primary");
1864
+ return machine;
1865
+ }
1866
+ async touchMachine(id) {
1867
+ const operation = "POST /v1/machines/:id/touch";
1868
+ const response = sdkObject(await this.post(`/api/machines/${encodeURIComponent(id)}/touch`), operation);
1869
+ if (response["contract"] !== MEMENTOS_MACHINE_TOUCH_CONTRACT || response["touched"] !== true || response["id"] !== id) {
1870
+ throw sdkProtocolError(operation, "expected touched=true for the requested stable id");
1871
+ }
1872
+ const machine = sdkMachine(response["machine"], operation);
1873
+ if (machine.id !== id || response["touched_at"] !== machine.last_seen_at) {
1874
+ throw sdkProtocolError(operation, "touch receipt does not match the returned machine");
1875
+ }
1876
+ return machine;
1877
+ }
1878
+ async deleteMachine(id) {
1879
+ const operation = "DELETE /v1/machines/:id";
1880
+ const response = sdkObject(await this.delete(`/api/machines/${encodeURIComponent(id)}`), operation);
1881
+ if (response["contract"] !== MEMENTOS_MACHINE_MUTATION_CONTRACT || response["deleted"] !== true || response["id"] !== id) {
1882
+ throw sdkProtocolError(operation, "expected deleted=true for the requested stable id");
1883
+ }
1884
+ return { deleted: true, id };
1885
+ }
269
1886
  listProjects() {
270
1887
  return this.get("/api/projects");
271
1888
  }
@@ -486,30 +2103,30 @@ class MementosClient {
486
2103
  }
487
2104
  async acquireLock(input) {
488
2105
  try {
489
- return await this.post("/api/locks", input);
490
- } catch (e) {
491
- if (e instanceof Error && e.message.includes("409"))
2106
+ return decodeResourceLock(await this.post("/api/locks", input), "lock acquire");
2107
+ } catch (error) {
2108
+ if (error instanceof MementosError && error.status === 409)
492
2109
  return null;
493
- throw e;
2110
+ throw error;
494
2111
  }
495
2112
  }
496
- checkLock(resourceType, resourceId, lockType) {
2113
+ async checkLock(resourceType, resourceId, lockType) {
497
2114
  const params = { resource_type: resourceType, resource_id: resourceId };
498
2115
  if (lockType)
499
2116
  params["lock_type"] = lockType;
500
- return this.get("/api/locks", params);
2117
+ return decodeResourceLocks(await this.get("/api/locks", params), "lock list");
501
2118
  }
502
- releaseLock(lockId, agentId) {
503
- return this.request("DELETE", `/api/locks/${lockId}`, { agent_id: agentId });
2119
+ async releaseLock(lockId, agentId) {
2120
+ return decodeBooleanReceipt(await this.request("DELETE", `/api/locks/${encodeURIComponent(lockId)}`, { agent_id: agentId }), "released", "lock release");
504
2121
  }
505
- listAgentLocks(agentId) {
506
- return this.get(`/api/agents/${agentId}/locks`);
2122
+ async listAgentLocks(agentId) {
2123
+ return decodeResourceLocks(await this.get(`/api/agents/${encodeURIComponent(agentId)}/locks`), "agent lock list");
507
2124
  }
508
- releaseAllAgentLocks(agentId) {
509
- return this.request("DELETE", `/api/agents/${agentId}/locks`);
2125
+ async releaseAllAgentLocks(agentId) {
2126
+ return decodeCountReceipt(await this.request("DELETE", `/api/agents/${encodeURIComponent(agentId)}/locks`), "released", "agent lock release");
510
2127
  }
511
- cleanExpiredLocks() {
512
- return this.post("/api/locks/clean", {});
2128
+ async cleanExpiredLocks() {
2129
+ return decodeCountReceipt(await this.post("/api/locks/clean", {}), "cleaned", "lock cleanup");
513
2130
  }
514
2131
  createTask(input) {
515
2132
  return this.post("/api/tasks", input);
@@ -631,32 +2248,71 @@ class MementosClient {
631
2248
  rollbackSynthesis(runId) {
632
2249
  return this.post(`/api/synthesis/rollback/${runId}`, {});
633
2250
  }
634
- ingestSession(input) {
635
- return this.post("/api/sessions/ingest", input);
2251
+ async ingestSession(input) {
2252
+ return decodeSessionIngestReceipt(await this.post("/api/sessions/ingest", input), input.transcript, input.session_id);
636
2253
  }
637
- getSessionJob(jobId) {
638
- return this.get(`/api/sessions/jobs/${jobId}`);
2254
+ async getSessionJob(jobId) {
2255
+ return decodeSessionJob(await this.get(`/api/sessions/jobs/${encodeURIComponent(jobId)}`), 0);
639
2256
  }
640
- listSessionJobs(filter) {
2257
+ async listSessionJobs(filter) {
641
2258
  const params = new URLSearchParams;
642
2259
  if (filter?.agent_id)
643
2260
  params.set("agent_id", filter.agent_id);
644
2261
  if (filter?.project_id)
645
2262
  params.set("project_id", filter.project_id);
2263
+ if (filter?.session_id)
2264
+ params.set("session_id", filter.session_id);
646
2265
  if (filter?.status)
647
2266
  params.set("status", filter.status);
648
- if (filter?.limit)
2267
+ if (filter?.limit !== undefined)
649
2268
  params.set("limit", String(filter.limit));
2269
+ if (filter?.offset !== undefined)
2270
+ params.set("offset", String(filter.offset));
650
2271
  const qs = params.toString() ? `?${params.toString()}` : "";
651
- return this.get(`/api/sessions/jobs${qs}`);
2272
+ const response = await this.get(`/api/sessions/jobs${qs}`);
2273
+ const page = decodeSessionJobsPage(response);
2274
+ if (filter?.limit !== undefined && page.limit !== filter.limit) {
2275
+ throw sessionProtocolError("server did not preserve requested 'limit'");
2276
+ }
2277
+ if (filter?.offset !== undefined && page.offset !== filter.offset) {
2278
+ throw sessionProtocolError("server did not preserve requested 'offset'");
2279
+ }
2280
+ for (const job of page.jobs) {
2281
+ if (filter?.agent_id !== undefined && job.agent_id !== filter.agent_id) {
2282
+ throw sessionProtocolError("server did not preserve requested 'agent_id'");
2283
+ }
2284
+ if (filter?.project_id !== undefined && job.project_id !== filter.project_id) {
2285
+ throw sessionProtocolError("server did not preserve requested 'project_id'");
2286
+ }
2287
+ if (filter?.session_id !== undefined && job.session_id !== filter.session_id) {
2288
+ throw sessionProtocolError("server did not preserve requested 'session_id'");
2289
+ }
2290
+ if (filter?.status !== undefined && job.status !== filter.status) {
2291
+ throw sessionProtocolError("server did not preserve requested 'status'");
2292
+ }
2293
+ }
2294
+ return page;
652
2295
  }
653
- getSessionQueueStats() {
654
- return this.get("/api/sessions/queue/stats");
2296
+ async getSessionQueueStats() {
2297
+ return decodeQueueStats(await this.get("/api/sessions/queue/stats"));
655
2298
  }
656
2299
  }
657
2300
  var sdk_default = MementosClient;
658
2301
  export {
2302
+ resolveMementosSdkTransport,
2303
+ resolveMementosApiBase,
659
2304
  sdk_default as default,
2305
+ __resetMementosSdkLocalNotice,
2306
+ SESSION_JOBS_PAGE_CONTRACT,
660
2307
  MementosError,
661
- MementosClient
2308
+ MementosConfigError,
2309
+ MementosClient,
2310
+ MEMENTOS_MACHINE_TOUCH_CONTRACT,
2311
+ MEMENTOS_MACHINE_REGISTRATION_CONTRACT,
2312
+ MEMENTOS_MACHINE_MUTATION_CONTRACT,
2313
+ MEMENTOS_MACHINE_LIST_CONTRACT,
2314
+ MEMENTOS_DEFAULT_BASE_URL,
2315
+ AUDIT_TRAIL_CONTRACT as MEMENTOS_AUDIT_TRAIL_CONTRACT,
2316
+ AUDIT_STATS_CONTRACT as MEMENTOS_AUDIT_STATS_CONTRACT,
2317
+ AUDIT_EXPORT_CONTRACT as MEMENTOS_AUDIT_EXPORT_CONTRACT
662
2318
  };