@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.
- package/README.md +119 -30
- package/bun.lock +4 -65
- package/dist/audit-contract.d.ts +87 -0
- package/dist/audit-contract.d.ts.map +1 -0
- package/dist/cli/commands/info-context.d.ts.map +1 -1
- package/dist/cli/commands/info-history.d.ts.map +1 -1
- package/dist/cli/commands/info-stale.d.ts.map +1 -1
- package/dist/cli/commands/io-backup.d.ts.map +1 -1
- package/dist/cli/commands/io-export.d.ts.map +1 -1
- package/dist/cli/commands/io-restore.d.ts.map +1 -1
- package/dist/cli/commands/memory-cmd-chain.d.ts.map +1 -1
- package/dist/cli/commands/memory-cmd-crud.d.ts.map +1 -1
- package/dist/cli/commands/memory-cmd-list.d.ts +25 -0
- package/dist/cli/commands/memory-cmd-list.d.ts.map +1 -1
- package/dist/cli/commands/memory-cmd-recall.d.ts.map +1 -1
- package/dist/cli/commands/memory-cmd-search.d.ts.map +1 -1
- package/dist/cli/commands/memory-cmd-tail.d.ts.map +1 -1
- package/dist/cli/commands/memory-cmd-view.d.ts.map +1 -1
- package/dist/cli/commands/memory-cmd-when-to-use.d.ts.map +1 -1
- package/dist/cli/commands/project.d.ts.map +1 -1
- package/dist/cli/commands/storage.d.ts.map +1 -1
- package/dist/cli/commands/system-doctor.d.ts.map +1 -1
- package/dist/cli/commands/system-session.d.ts.map +1 -1
- package/dist/cli/commands/system-status.d.ts +43 -0
- package/dist/cli/commands/system-status.d.ts.map +1 -0
- package/dist/cli/commands/system-synthesized-profile.d.ts.map +1 -1
- package/dist/cli/commands/system.d.ts.map +1 -1
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/index.js +4964 -2168
- package/dist/cli/register-all.d.ts +1 -1
- package/dist/cli/register-all.d.ts.map +1 -1
- package/dist/db/api-mode.d.ts +145 -37
- package/dist/db/api-mode.d.ts.map +1 -1
- package/dist/db/api-response-contract.d.ts +23 -0
- package/dist/db/api-response-contract.d.ts.map +1 -0
- package/dist/db/audit.d.ts +34 -45
- package/dist/db/audit.d.ts.map +1 -1
- package/dist/db/database.d.ts +1 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/locks.d.ts.map +1 -1
- package/dist/db/machines.d.ts +50 -13
- package/dist/db/machines.d.ts.map +1 -1
- package/dist/db/memories.d.ts.map +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/pg-migrate.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/session-jobs.d.ts +2 -0
- package/dist/db/session-jobs.d.ts.map +1 -1
- package/dist/db/store-backend.d.ts +8 -2
- package/dist/db/store-backend.d.ts.map +1 -1
- package/dist/diagnostics/historical-project-registration-receipt.js +299 -50
- package/dist/generated/storage-kit/backend.d.ts +12 -2
- package/dist/generated/storage-kit/backend.d.ts.map +1 -1
- package/dist/generated/storage-kit/index.d.ts +1 -1
- package/dist/generated/storage-kit/index.d.ts.map +1 -1
- package/dist/generated/storage-kit/migrations.d.ts +21 -0
- package/dist/generated/storage-kit/migrations.d.ts.map +1 -1
- package/dist/generated/storage-kit/pool.d.ts +2 -5
- package/dist/generated/storage-kit/pool.d.ts.map +1 -1
- package/dist/generated/storage-kit/query.d.ts +1 -1
- package/dist/generated/storage-kit/query.d.ts.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4328 -1485
- package/dist/lib/built-in-hooks.d.ts.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/env.d.ts +10 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/gatherer.d.ts.map +1 -1
- package/dist/lib/injection-project.d.ts +10 -0
- package/dist/lib/injection-project.d.ts.map +1 -0
- package/dist/lib/injector.d.ts +1 -0
- package/dist/lib/injector.d.ts.map +1 -1
- package/dist/lib/local-opt-in.d.ts +114 -0
- package/dist/lib/local-opt-in.d.ts.map +1 -0
- package/dist/lib/machine-visibility.d.ts.map +1 -1
- package/dist/lib/memory-lock.d.ts.map +1 -1
- package/dist/lib/paths.d.ts +9 -0
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/lib/profile-synthesizer.d.ts +13 -2
- package/dist/lib/profile-synthesizer.d.ts.map +1 -1
- package/dist/lib/redact.d.ts +70 -10
- package/dist/lib/redact.d.ts.map +1 -1
- package/dist/lib/reflection.d.ts.map +1 -1
- package/dist/lib/session-queue.d.ts.map +1 -1
- package/dist/lib/session-registry.d.ts +38 -1
- package/dist/lib/session-registry.d.ts.map +1 -1
- package/dist/lib/storage-sync.d.ts +5 -0
- package/dist/lib/storage-sync.d.ts.map +1 -1
- package/dist/lib/synthesis/index.d.ts.map +1 -1
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +6215 -2598
- package/dist/mcp/profile.d.ts +23 -0
- package/dist/mcp/profile.d.ts.map +1 -0
- package/dist/mcp/tools/lock-tools.d.ts.map +1 -1
- package/dist/mcp/tools/memory-audit.d.ts +1 -1
- package/dist/mcp/tools/memory-audit.d.ts.map +1 -1
- package/dist/mcp/tools/memory-crud.d.ts.map +1 -1
- package/dist/mcp/tools/memory-inject.d.ts.map +1 -1
- package/dist/mcp/tools/project-tools.d.ts.map +1 -1
- package/dist/mcp/tools/session-tools.d.ts.map +1 -1
- package/dist/mcp/tools/tool-registry.d.ts +22 -19
- package/dist/mcp/tools/tool-registry.d.ts.map +1 -1
- package/dist/mcp/tools/utility-tools.d.ts +2 -1
- package/dist/mcp/tools/utility-tools.d.ts.map +1 -1
- package/dist/project-registration.js +549 -205
- package/dist/sdk/index.d.ts +282 -14
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +1691 -35
- package/dist/server/auth.d.ts +2 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/helpers.d.ts +21 -7
- package/dist/server/helpers.d.ts.map +1 -1
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +6040 -2475
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/routes/audit.d.ts +2 -0
- package/dist/server/routes/audit.d.ts.map +1 -0
- package/dist/server/routes/machines.d.ts +2 -0
- package/dist/server/routes/machines.d.ts.map +1 -0
- package/dist/server/routes/system-sessions.d.ts.map +1 -1
- package/dist/server/routes/system-synthesis.d.ts.map +1 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +78 -129
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/docker-entrypoint.sh +7 -0
- package/hasna.contract.json +19 -7
- package/package.json +12 -11
- package/postinstall.mjs +52 -1
- package/dashboard/bun.lock +0 -519
- package/dist/cli/__fixtures__/clean-fallback-stub-server.d.ts +0 -2
- package/dist/cli/__fixtures__/clean-fallback-stub-server.d.ts.map +0 -1
- package/dist/cli/__fixtures__/io-restore-stub-server.d.ts +0 -2
- package/dist/cli/__fixtures__/io-restore-stub-server.d.ts.map +0 -1
- package/dist/cli/brains.d.ts +0 -3
- package/dist/cli/brains.d.ts.map +0 -1
- package/dist/db/__fixtures__/fail-closed-stub-server.d.ts +0 -2
- package/dist/db/__fixtures__/fail-closed-stub-server.d.ts.map +0 -1
- package/dist/db/__fixtures__/list-filter-capture-server.d.ts +0 -2
- package/dist/db/__fixtures__/list-filter-capture-server.d.ts.map +0 -1
- package/dist/db/__fixtures__/list-filter-client-runner.d.ts +0 -2
- package/dist/db/__fixtures__/list-filter-client-runner.d.ts.map +0 -1
- package/dist/lib/retired-storage-mode.d.ts +0 -11
- package/dist/lib/retired-storage-mode.d.ts.map +0 -1
- package/dist/test-support/memories-page-stub-server.d.ts +0 -2
- package/dist/test-support/memories-page-stub-server.d.ts.map +0 -1
- package/dist/test-support/memories-page-stub.d.ts +0 -28
- package/dist/test-support/memories-page-stub.d.ts.map +0 -1
- package/dist/test-support/pg-sync-stub-worker.d.ts +0 -2
- package/dist/test-support/pg-sync-stub-worker.d.ts.map +0 -1
- package/dist/test-support/preload-local-store.d.ts +0 -2
- package/dist/test-support/preload-local-store.d.ts.map +0 -1
- package/dist/test-support/project-authority-identity.d.ts +0 -8
- package/dist/test-support/project-authority-identity.d.ts.map +0 -1
- package/dist/test-support/store-isolation.d.ts +0 -108
- package/dist/test-support/store-isolation.d.ts.map +0 -1
- package/dist/test-support/strip-ansi.d.ts +0 -3
- 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
|
-
|
|
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 =
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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 = `${
|
|
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 (
|
|
48
|
-
headers["Authorization"] = `Bearer ${
|
|
49
|
-
headers["x-api-key"] =
|
|
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 (
|
|
491
|
-
if (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|