@jini-ai/platform 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/asset-cache.d.ts +12 -1
  2. package/dist/asset-cache.js +9 -5
  3. package/dist/child-environment.d.ts +18 -0
  4. package/dist/child-environment.js +26 -0
  5. package/dist/fs/boot-identity.d.ts +10 -0
  6. package/dist/fs/boot-identity.js +34 -0
  7. package/dist/fs/directory-home.d.ts +15 -0
  8. package/dist/fs/directory-home.js +17 -0
  9. package/dist/fs/directory-registry.d.ts +50 -0
  10. package/dist/fs/directory-registry.js +99 -0
  11. package/dist/fs/directory-running-lock.d.ts +43 -0
  12. package/dist/fs/directory-running-lock.js +66 -0
  13. package/dist/fs/directory-sync.d.ts +20 -0
  14. package/dist/fs/directory-sync.js +87 -0
  15. package/dist/fs/directory-user-config.d.ts +79 -0
  16. package/dist/fs/directory-user-config.js +183 -0
  17. package/dist/fs/directory-workspace.d.ts +132 -0
  18. package/dist/fs/directory-workspace.js +266 -0
  19. package/dist/fs/durable-json.d.ts +21 -0
  20. package/dist/fs/durable-json.js +59 -4
  21. package/dist/fs/file-lock.d.ts +3 -0
  22. package/dist/fs/file-lock.js +16 -7
  23. package/dist/fs/index.d.ts +6 -0
  24. package/dist/fs/index.js +6 -0
  25. package/dist/fs/user-data-directories.d.ts +28 -0
  26. package/dist/fs/user-data-directories.js +53 -0
  27. package/dist/fs/verified-directory-move.d.ts +37 -0
  28. package/dist/fs/verified-directory-move.js +138 -0
  29. package/dist/fs/verified-file-relocation.d.ts +27 -0
  30. package/dist/fs/verified-file-relocation.js +115 -0
  31. package/dist/fs/workspace-directory.d.ts +19 -0
  32. package/dist/fs/workspace-directory.js +23 -0
  33. package/dist/index.d.ts +2 -1
  34. package/dist/index.js +2 -1
  35. package/dist/secrets/credential-sets/index.d.ts +5 -0
  36. package/dist/secrets/credential-sets/index.js +5 -0
  37. package/dist/secrets/credential-sets/vault.aad.d.ts +4 -0
  38. package/dist/secrets/credential-sets/vault.aad.js +8 -0
  39. package/dist/secrets/credential-sets/vault.meta.d.ts +10 -0
  40. package/dist/secrets/credential-sets/vault.meta.js +52 -0
  41. package/dist/secrets/credential-sets/vault.repo.memory.d.ts +51 -0
  42. package/dist/secrets/credential-sets/vault.repo.memory.js +148 -0
  43. package/dist/secrets/credential-sets/vault.service.d.ts +130 -0
  44. package/dist/secrets/credential-sets/vault.service.js +310 -0
  45. package/dist/secrets/credential-sets/vault.types.d.ts +150 -0
  46. package/dist/secrets/credential-sets/vault.types.js +28 -0
  47. package/dist/web/html-document.d.ts +70 -0
  48. package/dist/web/html-document.js +268 -0
  49. package/dist/web/html-resource.d.ts +15 -0
  50. package/dist/web/html-resource.js +12 -0
  51. package/dist/web/html-to-markdown.d.ts +11 -0
  52. package/dist/web/html-to-markdown.js +245 -0
  53. package/dist/web/index.d.ts +5 -0
  54. package/dist/web/index.js +5 -0
  55. package/dist/web/web-page.d.ts +157 -0
  56. package/dist/web/web-page.js +310 -0
  57. package/package.json +14 -3
@@ -0,0 +1,310 @@
1
+ import { credentialTokenHint } from "../credential-token.js";
2
+ import { buildVaultAad } from "./vault.aad.js";
3
+ import { isCopyableKind } from "./vault.meta.js";
4
+ import { VAULT_KINDS, VaultError } from "./vault.types.js";
5
+ function assertContext(context) {
6
+ if (typeof context.workspaceId !== "string" || !context.workspaceId || typeof context.ownerKey !== "string" || !context.ownerKey ||
7
+ !VAULT_KINDS.includes(context.kind) || (context.principalId !== null && typeof context.principalId !== "string") ||
8
+ ("binding" in context && context.binding !== null && typeof context.binding !== "string") ||
9
+ (context.kind === "personal-execution" && !context.principalId))
10
+ throw new VaultError({ code: "context" });
11
+ }
12
+ function summary(record) {
13
+ // Enumerate the read model: an adapter's extra runtime properties cannot become API fields.
14
+ return { workspaceId: record.workspaceId, id: record.id, kind: record.kind, ownerKey: record.ownerKey,
15
+ principalId: record.principalId, binding: record.binding, provider: record.provider, label: record.label,
16
+ isDefault: record.isDefault, revision: record.revision, tokenLength: record.tokenLength,
17
+ last4: record.tokenLength !== null && record.tokenLength >= 12 ? record.last4?.slice(-4) ?? null : null,
18
+ meta: record.meta, createdAt: record.createdAt, updatedAt: record.updatedAt, configured: record.sealed !== null };
19
+ }
20
+ /** Shared vault lifecycle. Policy/catalogs are injected; persistence and encryption are ports.
21
+ * Expected context is checked before opening. Normal reads have no legacy-AAD fallback.
22
+ * Metadata reads never need keys, and every merged patch is parsed before CAS persistence.
23
+ */
24
+ export class SecretsVaultService {
25
+ deps;
26
+ constructor(deps, _options = {}) {
27
+ this.deps = deps;
28
+ }
29
+ authorize(input) {
30
+ const context = input.context;
31
+ // Policy adapters receive only scope, never a put payload or a metadata patch through an
32
+ // extra-property alias. This also keeps ordinary authorization diagnostics secret-free.
33
+ return this.deps.authorize({ action: input.action, context: { workspaceId: context.workspaceId,
34
+ kind: context.kind, ownerKey: context.ownerKey, principalId: context.principalId,
35
+ ...("binding" in context ? { binding: context.binding } : {}),
36
+ ...("id" in context ? { id: context.id } : {}),
37
+ ...("provider" in context && context.provider !== undefined ? { provider: context.provider } : {}) } });
38
+ }
39
+ parse(record) {
40
+ assertContext(record);
41
+ return { ...record, meta: this.deps.metaParsers[record.kind]({ meta: record.meta }) };
42
+ }
43
+ async find(input) {
44
+ const record = await this.deps.repo.find({ workspaceId: input.workspaceId, id: input.id });
45
+ if (!record)
46
+ return null;
47
+ if (buildVaultAad(input) !== buildVaultAad(record))
48
+ throw new VaultError({ code: "context" });
49
+ return this.parse(record);
50
+ }
51
+ async seal(input) {
52
+ if (input.payload === null)
53
+ return null;
54
+ // JSON round-trip makes the payload contract explicit; encryption never receives an object
55
+ // coerced to '[object Object]' or a connection URI masquerading as an entire payload.
56
+ if (typeof input.payload !== "object" || Array.isArray(input.payload))
57
+ throw new VaultError({ code: "invalid-record" });
58
+ const plaintext = JSON.stringify(input.payload);
59
+ return this.deps.sealer.seal({ plaintext, key: await this.deps.keyring.activeKey({}), aad: buildVaultAad(input.context) });
60
+ }
61
+ async open(record) {
62
+ if (!record.sealed)
63
+ return null;
64
+ const plaintext = await this.deps.sealer.open({ sealed: record.sealed }, { aad: buildVaultAad(record) });
65
+ let payload;
66
+ try {
67
+ payload = JSON.parse(plaintext);
68
+ }
69
+ catch {
70
+ throw new VaultError({ code: "invalid-record" });
71
+ }
72
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload))
73
+ throw new VaultError({ code: "invalid-record" });
74
+ return payload;
75
+ }
76
+ /** Create/replace configuration and a sealed JSON payload. Throws on policy, validation or CAS failure. */
77
+ async put(input, _options = {}) {
78
+ const record = await this.prepare(input);
79
+ return summary(this.parse(await this.deps.repo.put({ record, expectedRevision: input.expectedRevision })));
80
+ }
81
+ /** Authorize, validate and seal before the host enters its transaction, without persisting.
82
+ * Replacements retain identity/createdAt; omitted payload preserves the existing envelope.
83
+ * Commit must CAS again because preparation does not reserve a revision or a default.
84
+ * @example const record = await vault.prepare(input); // pass to commitPrepared inside the host transaction
85
+ */
86
+ async prepare(input, _options = {}) {
87
+ assertContext(input);
88
+ await this.authorize({ action: "put", context: input });
89
+ if (input.expectedRevision !== null && !input.id)
90
+ throw new VaultError({ code: "conflict" });
91
+ const identity = { workspaceId: input.workspaceId, kind: input.kind, ownerKey: input.ownerKey,
92
+ principalId: input.principalId, binding: input.binding, id: input.id ?? this.deps.newId({}) };
93
+ const existing = await this.find(identity);
94
+ if (input.expectedRevision === null ? existing !== null : existing?.revision !== input.expectedRevision)
95
+ throw new VaultError({ code: "conflict" });
96
+ const meta = this.deps.metaParsers[input.kind]({ meta: input.meta });
97
+ if (input.payload === undefined && !existing)
98
+ throw new VaultError({ code: "invalid-record" });
99
+ const sealed = input.payload === undefined ? existing.sealed : await this.seal({ context: identity, payload: input.payload });
100
+ // Never expose any suffix of a database address, environment map or OAuth session.
101
+ const hint = sealed && input.hintToken !== undefined && !["database-destination", "mcp-env", "mcp-auth"].includes(input.kind)
102
+ ? credentialTokenHint({ token: input.hintToken }) : null;
103
+ const now = this.deps.now({});
104
+ const record = { ...identity, provider: input.provider, label: input.label, meta, sealed,
105
+ isDefault: input.isDefault ?? (existing !== null && existing.provider === input.provider && existing.isDefault),
106
+ revision: (input.expectedRevision ?? 0) + 1,
107
+ last4: input.payload === undefined ? existing.last4 : hint?.last4 ?? null,
108
+ tokenLength: input.payload === undefined ? existing.tokenLength : hint?.length ?? null,
109
+ createdAt: existing?.createdAt ?? now, updatedAt: now };
110
+ return record;
111
+ }
112
+ /** Reauthorize prepared rows, validate ownership/metadata, then commit through the batch owner.
113
+ * No plaintext is opened or sealed. Missing capability/revisions and duplicate identities fail
114
+ * before writes; stale CAS or a later operation failure rolls back the whole repository batch.
115
+ * @example await vault.commitPrepared({ records: [record], deletes: [], expectedRevisions: { [record.id]: null } });
116
+ */
117
+ async commitPrepared(input, _options = {}) {
118
+ if (!this.deps.repo.applyBatch)
119
+ throw new VaultError({ code: "invalid-record" });
120
+ const { records, deletes, expectedRevisions } = structuredClone(input);
121
+ const identities = new Set();
122
+ const ops = [];
123
+ const entries = [...records.map(record => ({ put: record })), ...deletes.map(record => ({ delete: record }))];
124
+ for (const entry of entries) {
125
+ const record = "put" in entry ? entry.put : entry.delete;
126
+ assertContext(record);
127
+ const key = JSON.stringify([record.workspaceId, record.id]);
128
+ if (identities.has(key) || !Object.hasOwn(expectedRevisions, record.id))
129
+ throw new VaultError({ code: "invalid-record" });
130
+ identities.add(key);
131
+ const expectedRevision = expectedRevisions[record.id];
132
+ const putting = "put" in entry;
133
+ await this.authorize({ action: putting ? "put" : "delete", context: record });
134
+ const existing = await this.find(record);
135
+ if (expectedRevision === null ? existing !== null : existing?.revision !== expectedRevision)
136
+ throw new VaultError({ code: "conflict" });
137
+ if ("put" in entry)
138
+ ops.push({ put: { record: this.parse(entry.put), expectedRevision } });
139
+ else {
140
+ if (expectedRevision === null)
141
+ throw new VaultError({ code: "invalid-record" });
142
+ ops.push({ delete: { workspaceId: record.workspaceId, id: record.id, expectedRevision } });
143
+ }
144
+ }
145
+ const results = await this.deps.repo.applyBatch({ ops });
146
+ return results.filter((result) => typeof result !== "boolean").map(summary);
147
+ }
148
+ /** Authorized runtime read; returns plaintext only to the caller that supplied exact ownership. */
149
+ async get(input, _options = {}) {
150
+ assertContext(input);
151
+ await this.authorize({ action: "get", context: input });
152
+ const record = await this.find(input);
153
+ return record ? { summary: summary(record), payload: await this.open(record) } : null;
154
+ }
155
+ /** Singleton runtime read using one provider-independent lookup. Full expected ownership and
156
+ * binding are required before policy/decryption; repo callers may use a broader selection scope.
157
+ * @example await vault.findOne({ workspaceId, kind: "site-assistant", ownerKey: "assistant", principalId: null, binding: null });
158
+ */
159
+ async findOne(input, _options = {}) {
160
+ assertContext(input);
161
+ await this.authorize({ action: "get", context: input });
162
+ if (!this.deps.repo.findOne)
163
+ throw new VaultError({ code: "invalid-record" });
164
+ const record = await this.deps.repo.findOne(input);
165
+ if (!record)
166
+ return null;
167
+ if (buildVaultAad({ ...input, id: record.id }) !== buildVaultAad(record))
168
+ throw new VaultError({ code: "context" });
169
+ const parsed = this.parse(record);
170
+ return { summary: summary(parsed), payload: await this.open(parsed) };
171
+ }
172
+ /** Authorized default read using one exact-group query; ownership is checked before opening.
173
+ * @throws On policy, context, metadata or authentication failure.
174
+ * @complexity One indexed row lookup, then O(payload size) decryption.
175
+ */
176
+ async findDefault(input, _options = {}) {
177
+ assertContext(input);
178
+ await this.authorize({ action: "get", context: input });
179
+ // Old injected ports retain their existing operations; default reads require this capability.
180
+ if (!this.deps.repo.findDefault)
181
+ throw new VaultError({ code: "invalid-record" });
182
+ const record = await this.deps.repo.findDefault(input);
183
+ if (!record)
184
+ return null;
185
+ if (!record.isDefault || record.provider !== input.provider || buildVaultAad({ ...input, id: record.id }) !== buildVaultAad(record)) {
186
+ throw new VaultError({ code: "context" });
187
+ }
188
+ const parsed = this.parse(record);
189
+ return { summary: summary(parsed), payload: await this.open(parsed) };
190
+ }
191
+ /** Copy selected rows into this service without exposing plaintext or envelopes. Each ID is
192
+ * attempted once; failures are isolated and raw adapter/policy/crypto errors never escape.
193
+ * Source reads are plain SELECTs; this operation never upgrades or writes the source.
194
+ * @throws VaultError for invalid scopes. Per-row failures use fixed reason codes.
195
+ * @complexity O(n + payload bytes), n unique IDs; sequential bounded row reads and CAS writes.
196
+ */
197
+ async copyToSite(input, _options = {}) {
198
+ assertContext(input.sourceContext);
199
+ assertContext(input.targetContext);
200
+ const results = [];
201
+ for (const id of new Set(input.ids))
202
+ results.push(await this.copyRow({ ...input, id }));
203
+ return results;
204
+ }
205
+ /** One transfer reuses both services' parsing/open/seal and the repository's default/CAS owner.
206
+ * @complexity Constant row queries plus O(payload size); no group read or source transaction.
207
+ */
208
+ async copyRow(input, _options = {}) {
209
+ const { source, sourceContext, targetContext, id } = input;
210
+ const result = { kind: sourceContext.kind, id };
211
+ if (!isCopyableKind[sourceContext.kind])
212
+ return { ...result, status: "skipped",
213
+ reasonCode: sourceContext.kind === "mcp-env" ? "requires-mcp-configuration" : "kind-not-copyable" };
214
+ try {
215
+ // Copy changes the site/key, never the credential's owning feature or endpoint. Principal
216
+ // identities are local to each installation and cannot be carried to the target.
217
+ if (sourceContext.kind !== targetContext.kind || sourceContext.ownerKey !== targetContext.ownerKey ||
218
+ sourceContext.binding !== targetContext.binding || sourceContext.principalId !== null || targetContext.principalId !== null ||
219
+ (source.deps.repo === this.deps.repo && sourceContext.workspaceId === targetContext.workspaceId)) {
220
+ throw new VaultError({ code: "context" });
221
+ }
222
+ const sourceIdentity = { ...sourceContext, id };
223
+ const targetIdentity = { ...targetContext, id };
224
+ await source.authorize({ action: "get", context: sourceIdentity });
225
+ await this.authorize({ action: "put", context: targetIdentity });
226
+ const raw = await source.deps.repo.find(sourceIdentity);
227
+ if (!raw)
228
+ return { ...result, status: "failed", reasonCode: "source-missing" };
229
+ if (buildVaultAad(sourceIdentity) !== buildVaultAad(raw))
230
+ throw new VaultError({ code: "context" });
231
+ // Check before parsing: normal parsers deliberately reject legacy-AAD metadata.
232
+ if (Object.hasOwn(raw.meta, "legacyAad"))
233
+ return { ...result, status: "failed", reasonCode: "source-needs-upgrade" };
234
+ if (input.sourceRevisions && Object.hasOwn(input.sourceRevisions, id) && input.sourceRevisions[id] !== raw.revision)
235
+ throw new VaultError({ code: "conflict" });
236
+ const record = source.parse(raw);
237
+ const existing = await this.find(targetIdentity);
238
+ if (existing && !input.overwrite)
239
+ return { ...result, status: "skipped", reasonCode: "target-exists" };
240
+ if (existing && existing.provider !== record.provider)
241
+ throw new VaultError({ code: "context" });
242
+ const expectedRevision = existing?.revision ?? null;
243
+ if (input.targetRevisions && Object.hasOwn(input.targetRevisions, id) && input.targetRevisions[id] !== expectedRevision)
244
+ throw new VaultError({ code: "conflict" });
245
+ const now = this.deps.now({});
246
+ const candidate = this.parse({ ...record, ...targetIdentity, isDefault: existing?.isDefault ?? false,
247
+ revision: (expectedRevision ?? 0) + 1, createdAt: existing?.createdAt ?? now, updatedAt: now });
248
+ candidate.sealed = await this.seal({ context: targetIdentity, payload: await source.open(record) });
249
+ // Reuse the source's hint policy without guessing a token field from a structured payload.
250
+ candidate.last4 = record.kind === "database-destination" ? null : summary(record).last4;
251
+ candidate.tokenLength = record.kind === "database-destination" ? null : record.tokenLength;
252
+ const saved = await this.deps.repo.put({ record: candidate, expectedRevision });
253
+ const last4 = summary(saved).last4;
254
+ return { ...result, status: "copied", ...(last4 ? { last4 } : {}) };
255
+ }
256
+ catch (error) {
257
+ return { ...result, status: "failed", reasonCode: error instanceof VaultError ? error.code : "copy-failed" };
258
+ }
259
+ }
260
+ /** Human-managed scoped inventory. No envelope or plaintext is opened or returned. */
261
+ async list(input, _options = {}) {
262
+ assertContext(input);
263
+ await this.authorize({ action: "list", context: input });
264
+ return (await this.deps.repo.list(input)).map(record => {
265
+ if (record.workspaceId !== input.workspaceId || record.kind !== input.kind || record.ownerKey !== input.ownerKey ||
266
+ record.principalId !== input.principalId || (input.provider !== undefined && record.provider !== input.provider))
267
+ throw new VaultError({ code: "context" });
268
+ return summary(this.parse(record));
269
+ });
270
+ }
271
+ /** Merge and validate complete metadata, then target only changed JSON keys with revision CAS. */
272
+ async patchMeta(input, _options = {}) {
273
+ assertContext(input);
274
+ await this.authorize({ action: "put", context: input });
275
+ const existing = await this.find(input);
276
+ if (existing?.revision !== input.expectedRevision)
277
+ throw new VaultError({ code: "conflict" });
278
+ const meta = this.deps.metaParsers[existing.kind]({ meta: { ...existing.meta, ...input.patch } });
279
+ // jsonSet can add/replace a key but cannot remove one. Reject parser omissions instead of
280
+ // silently persisting an unvalidated old field. Normalized values and defaults can be patched.
281
+ if (Object.keys({ ...existing.meta, ...input.patch }).some(key => !Object.hasOwn(meta, key)))
282
+ throw new VaultError({ code: "invalid-meta" });
283
+ const patch = Object.fromEntries(Object.entries(meta).filter(([key, value]) => JSON.stringify(value) !== JSON.stringify(existing.meta[key])));
284
+ return summary(this.parse(await this.deps.repo.patchMeta({ workspaceId: input.workspaceId, id: input.id,
285
+ expectedRevision: input.expectedRevision, patch, updatedAt: this.deps.now({}) })));
286
+ }
287
+ /** Re-seal using the active key; concurrent replacement wins through CAS. Metadata-only rows persist. */
288
+ async rotate(input, _options = {}) {
289
+ assertContext(input);
290
+ await this.authorize({ action: "rotate", context: input });
291
+ const existing = await this.find(input);
292
+ if (existing?.revision !== input.expectedRevision)
293
+ throw new VaultError({ code: "conflict" });
294
+ const sealed = await this.seal({ context: input, payload: await this.open(existing) });
295
+ return summary(this.parse(await this.deps.repo.put({ expectedRevision: input.expectedRevision,
296
+ record: { ...existing, sealed, revision: existing.revision + 1, updatedAt: this.deps.now({}) } })));
297
+ }
298
+ /** Idempotent for missing rows; existing rows require matching context and revision. */
299
+ async delete(input, _options = {}) {
300
+ assertContext(input);
301
+ await this.authorize({ action: "delete", context: input });
302
+ const existing = await this.find(input);
303
+ if (!existing)
304
+ return false;
305
+ if (existing.revision !== input.expectedRevision)
306
+ throw new VaultError({ code: "conflict" });
307
+ return this.deps.repo.delete({ workspaceId: input.workspaceId, id: input.id, expectedRevision: input.expectedRevision });
308
+ }
309
+ }
310
+ //# sourceMappingURL=vault.service.js.map
@@ -0,0 +1,150 @@
1
+ import type { SealedSecret } from "../types.js";
2
+ export declare const VAULT_KINDS: readonly ["site-assistant", "personal-execution", "media-provider", "custom", "publish-peer", "database-destination", "vendor", "source-control", "mcp-env", "mcp-auth"];
3
+ export type VaultKind = typeof VAULT_KINDS[number];
4
+ export type VaultJson = null | boolean | number | string | VaultJson[] | {
5
+ [key: string]: VaultJson;
6
+ };
7
+ export type VaultMeta = {
8
+ [key: string]: VaultJson;
9
+ };
10
+ export type VaultOptions = Record<string, never>;
11
+ /** Expected ownership is supplied by the caller, never reconstructed from an untrusted row. */
12
+ export interface VaultContext {
13
+ workspaceId: string;
14
+ kind: VaultKind;
15
+ ownerKey: string;
16
+ principalId: string | null;
17
+ /** Host-canonicalized endpoint/resource binding; null for credentials without such a binding. */
18
+ binding: string | null;
19
+ }
20
+ export interface VaultIdentity extends VaultContext {
21
+ id: string;
22
+ }
23
+ export interface VaultRecord extends VaultIdentity {
24
+ provider: string | null;
25
+ label: string;
26
+ isDefault: boolean;
27
+ revision: number;
28
+ sealed: SealedSecret | null;
29
+ last4: string | null;
30
+ tokenLength: number | null;
31
+ meta: VaultMeta;
32
+ createdAt: string;
33
+ updatedAt: string;
34
+ }
35
+ export type VaultSummary = Omit<VaultRecord, "sealed"> & {
36
+ configured: boolean;
37
+ };
38
+ export interface VaultQuery {
39
+ workspaceId: string;
40
+ kind: VaultKind;
41
+ ownerKey: string;
42
+ principalId: string | null;
43
+ provider?: string | null;
44
+ }
45
+ /** A default belongs to one exact group; omitted provider never means all providers. */
46
+ export interface VaultDefaultQuery extends VaultQuery {
47
+ provider: string | null;
48
+ }
49
+ /** Singleton lookup ignores provider/default; omitted owner/principal leave that dimension unfiltered.
50
+ * Hosts enforce singleton cardinality for their kind/scope. No binding is inferred by the service.
51
+ */
52
+ export interface VaultSingletonQuery {
53
+ workspaceId: string;
54
+ kind: VaultKind;
55
+ ownerKey?: string | undefined;
56
+ principalId?: string | null | undefined;
57
+ }
58
+ /** Ordered CAS operations; default side effects affect later operations' revisions. */
59
+ export type VaultBatchOp = {
60
+ put: {
61
+ record: VaultRecord;
62
+ expectedRevision: number | null;
63
+ };
64
+ } | {
65
+ delete: {
66
+ workspaceId: string;
67
+ id: string;
68
+ expectedRevision: number;
69
+ };
70
+ } | {
71
+ patchMeta: {
72
+ workspaceId: string;
73
+ id: string;
74
+ expectedRevision: number;
75
+ patch: VaultMeta;
76
+ updatedAt: string;
77
+ };
78
+ };
79
+ export type VaultBatchResult = VaultRecord | boolean;
80
+ export interface VaultCopyResult {
81
+ kind: VaultKind;
82
+ id: string;
83
+ status: "copied" | "skipped" | "failed";
84
+ last4?: string;
85
+ reasonCode?: VaultError["code"] | "source-needs-upgrade" | "source-missing" | "target-exists" | "kind-not-copyable" | "requires-mcp-configuration" | "copy-failed";
86
+ }
87
+ export type VaultMetaParser = (input: {
88
+ meta: unknown;
89
+ }, options?: VaultOptions) => VaultMeta;
90
+ export type VaultMetaParsers = Readonly<Record<VaultKind, VaultMetaParser>>;
91
+ /** Safe error codes never contain submitted values or encrypted material. */
92
+ export declare class VaultError extends Error {
93
+ readonly code: "conflict" | "context" | "invalid-meta" | "invalid-record";
94
+ constructor({ code }: {
95
+ code: VaultError["code"];
96
+ }, _options?: VaultOptions);
97
+ }
98
+ /** CAS is mandatory: null creates only; a number replaces/deletes only that revision.
99
+ * Defaults group by workspace/kind/owner/principal/provider. Every default change increments
100
+ * revision; promotion uses updatedAt descending, then id descending (binary UTF-8 order).
101
+ * All writes must join a host transaction when the adapter supports transaction composition.
102
+ */
103
+ export interface VaultRepoPort {
104
+ find(input: {
105
+ workspaceId: string;
106
+ id: string;
107
+ }, options?: VaultOptions): Promise<VaultRecord | null>;
108
+ /** Indexed exact-group lookup; returns at most one row without listing its siblings.
109
+ * Optional for compatibility with pre-C1 ports; both built-in adapters implement it.
110
+ */
111
+ findDefault?(input: VaultDefaultQuery, options?: VaultOptions): Promise<VaultRecord | null>;
112
+ /** Optional additions keep older injected ports source-compatible; built-in adapters implement both. */
113
+ findOne?(input: VaultSingletonQuery, options?: VaultOptions): Promise<VaultRecord | null>;
114
+ /** All operations commit together or throw with no published changes.
115
+ * SQL joins the host transaction: callers must propagate failures to roll that transaction back.
116
+ * @example await repo.applyBatch!({ ops: [{ put: { record, expectedRevision: null } }] });
117
+ */
118
+ applyBatch?(input: {
119
+ ops: readonly VaultBatchOp[];
120
+ }, options?: VaultOptions): Promise<VaultBatchResult[]>;
121
+ list(input: VaultQuery, options?: VaultOptions): Promise<VaultRecord[]>;
122
+ put(input: {
123
+ record: VaultRecord;
124
+ expectedRevision: number | null;
125
+ }, options?: VaultOptions): Promise<VaultRecord>;
126
+ /** Service-validated patch: top-level keys are plain JSON-path identifiers. */
127
+ patchMeta(input: {
128
+ workspaceId: string;
129
+ id: string;
130
+ expectedRevision: number;
131
+ patch: VaultMeta;
132
+ updatedAt: string;
133
+ }, options?: VaultOptions): Promise<VaultRecord>;
134
+ delete(input: {
135
+ workspaceId: string;
136
+ id: string;
137
+ expectedRevision: number;
138
+ }, options?: VaultOptions): Promise<boolean>;
139
+ }
140
+ /** Unambiguous scope encoding keeps adapter default/identity checks on the same contract. */
141
+ export declare function vaultGroupKey({ record }: {
142
+ record: VaultRecord;
143
+ }, _options?: VaultOptions): string;
144
+ /** Adapters guard CAS and immutable identity even for callers below the service. */
145
+ export declare function assertVaultReplacement({ record, existing, expectedRevision }: {
146
+ record: VaultRecord;
147
+ existing: VaultRecord | null;
148
+ expectedRevision: number | null;
149
+ }, _options?: VaultOptions): void;
150
+ //# sourceMappingURL=vault.types.d.ts.map
@@ -0,0 +1,28 @@
1
+ export const VAULT_KINDS = ["site-assistant", "personal-execution", "media-provider", "custom", "publish-peer",
2
+ "database-destination", "vendor", "source-control", "mcp-env", "mcp-auth"];
3
+ /** Safe error codes never contain submitted values or encrypted material. */
4
+ export class VaultError extends Error {
5
+ code;
6
+ constructor({ code }, _options = {}) {
7
+ super(`Secrets vault: ${code}`);
8
+ this.code = code;
9
+ this.name = "VaultError";
10
+ }
11
+ }
12
+ /** Unambiguous scope encoding keeps adapter default/identity checks on the same contract. */
13
+ export function vaultGroupKey({ record }, _options = {}) {
14
+ return JSON.stringify([record.workspaceId, record.kind, record.ownerKey, record.principalId, record.provider]);
15
+ }
16
+ /** Adapters guard CAS and immutable identity even for callers below the service. */
17
+ export function assertVaultReplacement({ record, existing, expectedRevision }, _options = {}) {
18
+ if (expectedRevision === null ? existing !== null : existing?.revision !== expectedRevision)
19
+ throw new VaultError({ code: "conflict" });
20
+ if (!Number.isSafeInteger(record.revision) || record.revision !== (expectedRevision ?? 0) + 1)
21
+ throw new VaultError({ code: "invalid-record" });
22
+ // Provider groups defaults, but is not authenticated identity (see buildVaultAad).
23
+ if (existing && (record.workspaceId !== existing.workspaceId || record.kind !== existing.kind ||
24
+ record.ownerKey !== existing.ownerKey || record.principalId !== existing.principalId ||
25
+ record.id !== existing.id || record.binding !== existing.binding || record.createdAt !== existing.createdAt))
26
+ throw new VaultError({ code: "context" });
27
+ }
28
+ //# sourceMappingURL=vault.types.js.map
@@ -0,0 +1,70 @@
1
+ import { type DefaultTreeAdapterMap } from "parse5";
2
+ type Element = DefaultTreeAdapterMap["element"];
3
+ export interface PageLink {
4
+ href: string;
5
+ text: string;
6
+ internal: boolean;
7
+ }
8
+ export interface PageImage {
9
+ src: string;
10
+ alt: string;
11
+ }
12
+ export interface HtmlDocumentFacts {
13
+ title?: string;
14
+ description?: string;
15
+ lang?: string;
16
+ links: PageLink[];
17
+ linksTruncated: boolean;
18
+ images: PageImage[];
19
+ imagesTruncated: boolean;
20
+ stylesheets: string[];
21
+ meta: Record<string, string>;
22
+ }
23
+ export declare const MAX_LINKS = 500;
24
+ export declare const MAX_IMAGES = 300;
25
+ export declare const LINK_PROTOCOLS: ReadonlySet<string>;
26
+ export declare const ASSET_PROTOCOLS: ReadonlySet<string>;
27
+ export declare function attr({ element, name }: {
28
+ element: Element;
29
+ name: string;
30
+ }, _optional?: Record<string, never>): string | undefined;
31
+ /** Resolves against `base`; drops fragment-only, unparseable and other-protocol values, and fragments. */
32
+ export declare function absolute({ raw, base, protocols }: {
33
+ raw: string | undefined;
34
+ base: string;
35
+ protocols: ReadonlySet<string>;
36
+ }, _optional?: Record<string, never>): string | undefined;
37
+ /** First usable image URL: lazy loaders park the real one in `data-src` behind a `data:` placeholder. */
38
+ export declare function imageSource({ img }: {
39
+ img: Element;
40
+ }, _optional?: Record<string, never>): string | undefined;
41
+ /**
42
+ * Extracts page facts. Relative URLs resolve against `<base href>` (itself resolved against the page
43
+ * URL) so links match what a browser would follow.
44
+ * @param required.html - Decoded page source; untrusted.
45
+ * @param required.pageUrl - The final URL the HTML was served from (after redirects).
46
+ * @returns Facts with every URL absolute; links deduped (fragment-free) and capped at {@link MAX_LINKS},
47
+ * images at {@link MAX_IMAGES}; `internal` ignores a leading `www.`.
48
+ * @complexity O(nodes) plus O(anchor subtree) per anchor for its text.
49
+ */
50
+ export declare function readHtmlDocument({ html, pageUrl }: {
51
+ html: string;
52
+ pageUrl: string;
53
+ }, _optional?: Record<string, never>): HtmlDocumentFacts;
54
+ /**
55
+ * Visible text with one line per block element; scripts, styles, `<head>`, SVG and embeds omitted.
56
+ * @complexity O(nodes).
57
+ */
58
+ export declare function htmlToText({ html }: {
59
+ html: string;
60
+ }, _optional?: Record<string, never>): string;
61
+ /**
62
+ * The page HTML with `<script>`, `<noscript>`, `<template>`, comments, `on*` handler attributes and
63
+ * `javascript:` attribute values removed (styles and classes kept: theming needs them). Re-serialized by parse5, so it is well-formed rather than byte-identical.
64
+ * @complexity O(nodes).
65
+ */
66
+ export declare function stripActiveHtml({ html }: {
67
+ html: string;
68
+ }, _optional?: Record<string, never>): string;
69
+ export {};
70
+ //# sourceMappingURL=html-document.d.ts.map