@jini-ai/platform 0.5.1 → 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 (74) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/asset-cache.d.ts +12 -1
  3. package/dist/asset-cache.js +9 -5
  4. package/dist/child-environment.d.ts +18 -0
  5. package/dist/child-environment.js +26 -0
  6. package/dist/fs/boot-identity.d.ts +10 -0
  7. package/dist/fs/boot-identity.js +34 -0
  8. package/dist/fs/directory-home.d.ts +15 -0
  9. package/dist/fs/directory-home.js +17 -0
  10. package/dist/fs/directory-registry.d.ts +50 -0
  11. package/dist/fs/directory-registry.js +99 -0
  12. package/dist/fs/directory-running-lock.d.ts +43 -0
  13. package/dist/fs/directory-running-lock.js +66 -0
  14. package/dist/fs/directory-sync.d.ts +20 -0
  15. package/dist/fs/directory-sync.js +87 -0
  16. package/dist/fs/directory-user-config.d.ts +79 -0
  17. package/dist/fs/directory-user-config.js +183 -0
  18. package/dist/fs/directory-workspace.d.ts +132 -0
  19. package/dist/fs/directory-workspace.js +266 -0
  20. package/dist/fs/durable-json.d.ts +21 -0
  21. package/dist/fs/durable-json.js +59 -4
  22. package/dist/fs/file-lock.d.ts +3 -0
  23. package/dist/fs/file-lock.js +16 -7
  24. package/dist/fs/index.d.ts +6 -0
  25. package/dist/fs/index.js +6 -0
  26. package/dist/fs/strict-containment.d.ts +1 -1
  27. package/dist/fs/strict-containment.js +1 -1
  28. package/dist/fs/user-data-directories.d.ts +28 -0
  29. package/dist/fs/user-data-directories.js +53 -0
  30. package/dist/fs/verified-directory-move.d.ts +37 -0
  31. package/dist/fs/verified-directory-move.js +138 -0
  32. package/dist/fs/verified-file-relocation.d.ts +27 -0
  33. package/dist/fs/verified-file-relocation.js +115 -0
  34. package/dist/fs/workspace-directory.d.ts +19 -0
  35. package/dist/fs/workspace-directory.js +23 -0
  36. package/dist/http/guarded/transport.fetch.d.ts +13 -1
  37. package/dist/http/guarded/transport.fetch.js +9 -3
  38. package/dist/index.d.ts +6 -54
  39. package/dist/index.js +6 -54
  40. package/dist/net/address.d.ts +1 -8
  41. package/dist/net/address.js +2 -89
  42. package/dist/net/endpoint-policy.d.ts +9 -0
  43. package/dist/net/endpoint-policy.js +98 -0
  44. package/dist/secrets/credential-sets/index.d.ts +5 -0
  45. package/dist/secrets/credential-sets/index.js +5 -0
  46. package/dist/secrets/credential-sets/repo.memory.d.ts +18 -10
  47. package/dist/secrets/credential-sets/repo.memory.js +18 -10
  48. package/dist/secrets/credential-sets/vault.aad.d.ts +4 -0
  49. package/dist/secrets/credential-sets/vault.aad.js +8 -0
  50. package/dist/secrets/credential-sets/vault.meta.d.ts +10 -0
  51. package/dist/secrets/credential-sets/vault.meta.js +52 -0
  52. package/dist/secrets/credential-sets/vault.repo.memory.d.ts +51 -0
  53. package/dist/secrets/credential-sets/vault.repo.memory.js +148 -0
  54. package/dist/secrets/credential-sets/vault.service.d.ts +130 -0
  55. package/dist/secrets/credential-sets/vault.service.js +310 -0
  56. package/dist/secrets/credential-sets/vault.types.d.ts +150 -0
  57. package/dist/secrets/credential-sets/vault.types.js +28 -0
  58. package/dist/secrets/credential-token.d.ts +64 -0
  59. package/dist/secrets/credential-token.js +75 -0
  60. package/dist/secrets/index.d.ts +1 -0
  61. package/dist/secrets/index.js +1 -0
  62. package/dist/secrets/secret-sealer.aesgcm.d.ts +6 -0
  63. package/dist/secrets/secret-sealer.aesgcm.js +9 -2
  64. package/dist/web/html-document.d.ts +70 -0
  65. package/dist/web/html-document.js +268 -0
  66. package/dist/web/html-resource.d.ts +15 -0
  67. package/dist/web/html-resource.js +12 -0
  68. package/dist/web/html-to-markdown.d.ts +11 -0
  69. package/dist/web/html-to-markdown.js +245 -0
  70. package/dist/web/index.d.ts +5 -0
  71. package/dist/web/index.js +5 -0
  72. package/dist/web/web-page.d.ts +157 -0
  73. package/dist/web/web-page.js +310 -0
  74. package/package.json +26 -3
@@ -135,95 +135,8 @@ export function isPrivateAddress({ address: addr }) {
135
135
  }
136
136
  return true; // not a literal IP — caller must resolve before trusting it
137
137
  }
138
- function normalizeBracketedIpv6(hostname) {
139
- const stripped = hostname.startsWith('[') && hostname.endsWith(']')
140
- ? hostname.slice(1, -1)
141
- : hostname;
142
- // FQDN trailing-dot form (RFC 1034) resolves identically to the dotless
143
- // form, so `localhost.` must normalize to `localhost` before the equality
144
- // check below — and `0.0.0.0.`, `10.0.0.1.`, etc. must normalize before
145
- // isBlockedIpv4 parses them. Strips one or more trailing dots.
146
- return stripped.toLowerCase().replace(/\.+$/, '');
147
- }
148
- function parseIpv4(hostname) {
149
- const parts = hostname.split('.');
150
- if (parts.length !== 4)
151
- return null;
152
- const parsed = parts.map((part) => {
153
- if (!/^\d{1,3}$/.test(part))
154
- return null;
155
- const value = Number(part);
156
- return value >= 0 && value <= 255 ? value : null;
157
- });
158
- if (parsed.some((part) => part === null))
159
- return null;
160
- return [parsed[0], parsed[1], parsed[2], parsed[3]];
161
- }
162
- function isLoopbackIpv4(hostname) {
163
- const parts = parseIpv4(hostname);
164
- return Boolean(parts && parts[0] === 127);
165
- }
166
- function isBlockedIpv4(hostname) {
167
- const parts = parseIpv4(hostname);
168
- if (!parts)
169
- return false;
170
- const [a, b] = parts;
171
- return (a === 0 ||
172
- (a === 100 && b >= 64 && b <= 127) ||
173
- (a === 169 && b === 254) ||
174
- a === 10 ||
175
- (a === 192 && b === 168) ||
176
- (a === 172 && b >= 16 && b <= 31) ||
177
- a >= 224);
178
- }
179
- function ipv4MappedToDotted(hostname) {
180
- const host = normalizeBracketedIpv6(hostname);
181
- const mapped = /^::ffff:(.+)$/i.exec(host)?.[1];
182
- if (!mapped)
183
- return null;
184
- if (parseIpv4(mapped.toLowerCase()))
185
- return mapped.toLowerCase();
186
- const hexParts = mapped.split(':');
187
- if (hexParts.length !== 2 ||
188
- !hexParts.every((part) => /^[0-9a-f]{1,4}$/i.test(part))) {
189
- return null;
190
- }
191
- // Non-null assertions, not a runtime guard: the length/regex checks above
192
- // already guarantee exactly two non-empty hex segments here.
193
- const hi = hexParts[0];
194
- const lo = hexParts[1];
195
- const value = (Number.parseInt(hi, 16) << 16) | Number.parseInt(lo, 16);
196
- return [
197
- (value >>> 24) & 255,
198
- (value >>> 16) & 255,
199
- (value >>> 8) & 255,
200
- value & 255,
201
- ].join('.');
202
- }
203
- /** True for `localhost`, `::1`, `127.0.0.0/8`, and their IPv4-mapped-IPv6 forms. */
204
- export function isLoopbackApiHost({ hostname }) {
205
- const host = normalizeBracketedIpv6(hostname);
206
- if (host === 'localhost' || host === '::1')
207
- return true;
208
- if (isLoopbackIpv4(host))
209
- return true;
210
- const mapped = ipv4MappedToDotted(host);
211
- return Boolean(mapped && isLoopbackIpv4(mapped));
212
- }
213
- /** True for RFC1918/link-local/CGNAT/multicast/unspecified/unique-local-IPv6 addresses — private network space a public caller should never be steered into. */
214
- export function isBlockedExternalApiHostname({ hostname }) {
215
- const host = normalizeBracketedIpv6(hostname);
216
- if (host === '::')
217
- return true;
218
- if (isBlockedIpv4(host))
219
- return true;
220
- if (/^f[cd][0-9a-f]{2}:/i.test(host))
221
- return true;
222
- if (/^fe[89ab][0-9a-f]:/i.test(host))
223
- return true;
224
- const mapped = ipv4MappedToDotted(host);
225
- return Boolean(mapped && isBlockedIpv4(mapped));
226
- }
138
+ // Preserve the Node entry's published names while sharing the literal-host owner with browser consumers.
139
+ export { isLoopbackApiHost, isBlockedExternalApiHostname } from './endpoint-policy.js';
227
140
  /** True when `hostname` is `localhost`, an IPv6 loopback literal, or a `127.0.0.0/8` IPv4 literal. */
228
141
  export function isLoopbackHostname({ hostname }, _optional = {}) {
229
142
  const normalized = String(hostname || '')
@@ -0,0 +1,9 @@
1
+ /** True for `localhost`, `::1`, `127.0.0.0/8`, and their IPv4-mapped-IPv6 forms. */
2
+ export declare function isLoopbackApiHost({ hostname }: {
3
+ hostname: string;
4
+ }): boolean;
5
+ /** True for RFC1918/link-local/CGNAT/multicast/unspecified/unique-local-IPv6 addresses — private network space a public caller should never be steered into. */
6
+ export declare function isBlockedExternalApiHostname({ hostname }: {
7
+ hostname: string;
8
+ }): boolean;
9
+ //# sourceMappingURL=endpoint-policy.d.ts.map
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Browser-safe literal endpoint hostname policy. No Node imports or DNS effects:
3
+ * operator-configured local model servers deliberately keep their loopback
4
+ * exception. Resolved-peer validation and strict IP parsing belong to the Node
5
+ * adapters, whose different trust boundaries must not be collapsed into this.
6
+ * SSRF peer classification is distinct from core's browser-development LAN origin allow-list.
7
+ */
8
+ function normalizeBracketedIpv6(hostname) {
9
+ const stripped = hostname.startsWith('[') && hostname.endsWith(']')
10
+ ? hostname.slice(1, -1)
11
+ : hostname;
12
+ // FQDN trailing-dot form (RFC 1034) resolves identically to the dotless
13
+ // form, so `localhost.` must normalize to `localhost` before the equality
14
+ // check below — and `0.0.0.0.`, `10.0.0.1.`, etc. must normalize before
15
+ // isBlockedIpv4 parses them. Strips one or more trailing dots.
16
+ return stripped.toLowerCase().replace(/\.+$/, '');
17
+ }
18
+ function parseIpv4(hostname) {
19
+ const parts = hostname.split('.');
20
+ if (parts.length !== 4)
21
+ return null;
22
+ const parsed = parts.map((part) => {
23
+ if (!/^\d{1,3}$/.test(part))
24
+ return null;
25
+ const value = Number(part);
26
+ return value >= 0 && value <= 255 ? value : null;
27
+ });
28
+ if (parsed.some((part) => part === null))
29
+ return null;
30
+ return [parsed[0], parsed[1], parsed[2], parsed[3]];
31
+ }
32
+ function isLoopbackIpv4(hostname) {
33
+ const parts = parseIpv4(hostname);
34
+ return Boolean(parts && parts[0] === 127);
35
+ }
36
+ function isBlockedIpv4(hostname) {
37
+ const parts = parseIpv4(hostname);
38
+ if (!parts)
39
+ return false;
40
+ const [a, b] = parts;
41
+ return (a === 0 ||
42
+ (a === 100 && b >= 64 && b <= 127) ||
43
+ (a === 169 && b === 254) ||
44
+ a === 10 ||
45
+ (a === 192 && b === 168) ||
46
+ (a === 172 && b >= 16 && b <= 31) ||
47
+ a >= 224);
48
+ }
49
+ /** `::ffff:10.0.0.1` and `::ffff:a00:1` denote the same address; return dotted form so the v4 rules apply. */
50
+ function ipv4MappedToDotted(hostname) {
51
+ const host = normalizeBracketedIpv6(hostname);
52
+ const mapped = /^::ffff:(.+)$/i.exec(host)?.[1];
53
+ if (!mapped)
54
+ return null;
55
+ if (parseIpv4(mapped.toLowerCase()))
56
+ return mapped.toLowerCase();
57
+ const hexParts = mapped.split(':');
58
+ if (hexParts.length !== 2 ||
59
+ !hexParts.every((part) => /^[0-9a-f]{1,4}$/i.test(part))) {
60
+ return null;
61
+ }
62
+ // Non-null assertions, not a runtime guard: the length/regex checks above
63
+ // already guarantee exactly two non-empty hex segments here.
64
+ const hi = hexParts[0];
65
+ const lo = hexParts[1];
66
+ const value = (Number.parseInt(hi, 16) << 16) | Number.parseInt(lo, 16);
67
+ return [
68
+ (value >>> 24) & 255,
69
+ (value >>> 16) & 255,
70
+ (value >>> 8) & 255,
71
+ value & 255,
72
+ ].join('.');
73
+ }
74
+ /** True for `localhost`, `::1`, `127.0.0.0/8`, and their IPv4-mapped-IPv6 forms. */
75
+ export function isLoopbackApiHost({ hostname }) {
76
+ const host = normalizeBracketedIpv6(hostname);
77
+ if (host === 'localhost' || host === '::1')
78
+ return true;
79
+ if (isLoopbackIpv4(host))
80
+ return true;
81
+ const mapped = ipv4MappedToDotted(host);
82
+ return Boolean(mapped && isLoopbackIpv4(mapped));
83
+ }
84
+ /** True for RFC1918/link-local/CGNAT/multicast/unspecified/unique-local-IPv6 addresses — private network space a public caller should never be steered into. */
85
+ export function isBlockedExternalApiHostname({ hostname }) {
86
+ const host = normalizeBracketedIpv6(hostname);
87
+ if (host === '::')
88
+ return true;
89
+ if (isBlockedIpv4(host))
90
+ return true;
91
+ if (/^f[cd][0-9a-f]{2}:/i.test(host))
92
+ return true;
93
+ if (/^fe[89ab][0-9a-f]:/i.test(host))
94
+ return true;
95
+ const mapped = ipv4MappedToDotted(host);
96
+ return Boolean(mapped && isBlockedIpv4(mapped));
97
+ }
98
+ //# sourceMappingURL=endpoint-policy.js.map
@@ -1,4 +1,9 @@
1
1
  export type { VendorCredentialSetRecord, VendorCredentialSetSummary, VendorCredentialSetRepoPort } from "./types.js";
2
2
  export { buildVendorCredentialAad } from "./aad.js";
3
3
  export { InMemoryVendorCredentialSetRepo } from "./repo.memory.js";
4
+ export * from "./vault.types.js";
5
+ export { buildVaultAad } from "./vault.aad.js";
6
+ export { createVaultMetaParsers, isCopyableKind } from "./vault.meta.js";
7
+ export { SecretsVaultService, type VaultServiceDeps, type VaultPutInput, type VaultCopyInput, type VaultCommitPreparedInput } from "./vault.service.js";
8
+ export { InMemoryVaultRepo } from "./vault.repo.memory.js";
4
9
  //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,8 @@
1
1
  export { buildVendorCredentialAad } from "./aad.js";
2
2
  export { InMemoryVendorCredentialSetRepo } from "./repo.memory.js";
3
+ export * from "./vault.types.js";
4
+ export { buildVaultAad } from "./vault.aad.js";
5
+ export { createVaultMetaParsers, isCopyableKind } from "./vault.meta.js";
6
+ export { SecretsVaultService } from "./vault.service.js";
7
+ export { InMemoryVaultRepo } from "./vault.repo.memory.js";
3
8
  //# sourceMappingURL=index.js.map
@@ -1,36 +1,44 @@
1
1
  import type { VendorCredentialSetRecord, VendorCredentialSetRepoPort } from "./types.js";
2
- /** In-memory repository preserving label uniqueness and group default promotion. */
2
+ /**
3
+ * @file `VendorCredentialSetRepoPort`'s in-memory adapter — the ADR-006 rule-of-two test double,
4
+ * mirroring `../deployments/publish-credentials/repo.memory.ts`'s shape byte-for-byte with
5
+ * `providerId` swapped for `vendorId`. See that file's own header for the full reasoning (row
6
+ * keying, the enforced UNIQUE constraint matching better-sqlite3's own error wording, why no
7
+ * explicit lock is needed for the `isDefault` group invariant in a single-threaded JS process).
8
+ */
3
9
  export declare class InMemoryVendorCredentialSetRepo implements VendorCredentialSetRepoPort {
4
- constructor(_required: Record<string, never>);
10
+ constructor(_required: Record<string, never>, _optional?: Record<string, never>);
5
11
  private readonly rows;
6
12
  private static rowKey;
7
13
  private assertLabelAvailable;
8
14
  private clearOtherDefaults;
9
- insert(record: VendorCredentialSetRecord): Promise<void>;
10
- update(record: VendorCredentialSetRecord): Promise<void>;
15
+ insert(record: VendorCredentialSetRecord, _optional?: Record<string, never>): Promise<void>;
16
+ update(record: VendorCredentialSetRecord, _optional?: Record<string, never>): Promise<void>;
11
17
  findById(input: {
12
18
  workspaceId: string;
13
19
  id: string;
14
- }): Promise<VendorCredentialSetRecord | null>;
20
+ }, _optional?: Record<string, never>): Promise<VendorCredentialSetRecord | null>;
15
21
  findDefaultByVendor(input: {
16
22
  workspaceId: string;
17
23
  vendorId: string;
18
- }): Promise<VendorCredentialSetRecord | null>;
24
+ }, _optional?: Record<string, never>): Promise<VendorCredentialSetRecord | null>;
19
25
  listByVendor(input: {
20
26
  workspaceId: string;
21
27
  vendorId: string;
22
- }): Promise<VendorCredentialSetRecord[]>;
28
+ }, _optional?: Record<string, never>): Promise<VendorCredentialSetRecord[]>;
23
29
  listByWorkspace(input: {
24
30
  workspaceId: string;
25
- }): Promise<VendorCredentialSetRecord[]>;
31
+ }, _optional?: Record<string, never>): Promise<VendorCredentialSetRecord[]>;
26
32
  delete(input: {
27
33
  workspaceId: string;
28
34
  id: string;
29
- }): Promise<void>;
35
+ }, _optional?: Record<string, never>): Promise<void>;
36
+ /** Mirrors the SQLite adapter's targeted single-column write — see
37
+ * `VendorCredentialSetRepoPort.updateAccountLabel`'s own doc. No-op if the row vanished. */
30
38
  updateAccountLabel(input: {
31
39
  workspaceId: string;
32
40
  id: string;
33
41
  accountLabel: string;
34
- }): Promise<void>;
42
+ }, _optional?: Record<string, never>): Promise<void>;
35
43
  }
36
44
  //# sourceMappingURL=repo.memory.d.ts.map
@@ -1,6 +1,12 @@
1
- /** In-memory repository preserving label uniqueness and group default promotion. */
1
+ /**
2
+ * @file `VendorCredentialSetRepoPort`'s in-memory adapter — the ADR-006 rule-of-two test double,
3
+ * mirroring `../deployments/publish-credentials/repo.memory.ts`'s shape byte-for-byte with
4
+ * `providerId` swapped for `vendorId`. See that file's own header for the full reasoning (row
5
+ * keying, the enforced UNIQUE constraint matching better-sqlite3's own error wording, why no
6
+ * explicit lock is needed for the `isDefault` group invariant in a single-threaded JS process).
7
+ */
2
8
  export class InMemoryVendorCredentialSetRepo {
3
- constructor(_required) { }
9
+ constructor(_required, _optional = {}) { }
4
10
  rows = new Map();
5
11
  static rowKey(workspaceId, id) {
6
12
  return `${workspaceId}::${id}`;
@@ -21,35 +27,35 @@ export class InMemoryVendorCredentialSetRepo {
21
27
  this.rows.set(key, { ...row, isDefault: false });
22
28
  }
23
29
  }
24
- async insert(record) {
30
+ async insert(record, _optional = {}) {
25
31
  this.assertLabelAvailable(record.workspaceId, record.vendorId, record.label);
26
32
  this.rows.set(InMemoryVendorCredentialSetRepo.rowKey(record.workspaceId, record.id), { ...record });
27
33
  if (record.isDefault)
28
34
  this.clearOtherDefaults(record.workspaceId, record.vendorId, record.id);
29
35
  }
30
- async update(record) {
36
+ async update(record, _optional = {}) {
31
37
  this.assertLabelAvailable(record.workspaceId, record.vendorId, record.label, record.id);
32
38
  this.rows.set(InMemoryVendorCredentialSetRepo.rowKey(record.workspaceId, record.id), { ...record });
33
39
  if (record.isDefault)
34
40
  this.clearOtherDefaults(record.workspaceId, record.vendorId, record.id);
35
41
  }
36
- async findById(input) {
42
+ async findById(input, _optional = {}) {
37
43
  return this.rows.get(InMemoryVendorCredentialSetRepo.rowKey(input.workspaceId, input.id)) ?? null;
38
44
  }
39
- async findDefaultByVendor(input) {
45
+ async findDefaultByVendor(input, _optional = {}) {
40
46
  for (const row of this.rows.values()) {
41
47
  if (row.workspaceId === input.workspaceId && row.vendorId === input.vendorId && row.isDefault)
42
48
  return row;
43
49
  }
44
50
  return null;
45
51
  }
46
- async listByVendor(input) {
52
+ async listByVendor(input, _optional = {}) {
47
53
  return [...this.rows.values()].filter((row) => row.workspaceId === input.workspaceId && row.vendorId === input.vendorId);
48
54
  }
49
- async listByWorkspace(input) {
55
+ async listByWorkspace(input, _optional = {}) {
50
56
  return [...this.rows.values()].filter((row) => row.workspaceId === input.workspaceId);
51
57
  }
52
- async delete(input) {
58
+ async delete(input, _optional = {}) {
53
59
  const key = InMemoryVendorCredentialSetRepo.rowKey(input.workspaceId, input.id);
54
60
  const removed = this.rows.get(key);
55
61
  this.rows.delete(key);
@@ -61,7 +67,9 @@ export class InMemoryVendorCredentialSetRepo {
61
67
  const promoted = remaining.reduce((latest, row) => (row.updatedAt > latest.updatedAt ? row : latest));
62
68
  this.rows.set(InMemoryVendorCredentialSetRepo.rowKey(promoted.workspaceId, promoted.id), { ...promoted, isDefault: true });
63
69
  }
64
- async updateAccountLabel(input) {
70
+ /** Mirrors the SQLite adapter's targeted single-column write — see
71
+ * `VendorCredentialSetRepoPort.updateAccountLabel`'s own doc. No-op if the row vanished. */
72
+ async updateAccountLabel(input, _optional = {}) {
65
73
  const key = InMemoryVendorCredentialSetRepo.rowKey(input.workspaceId, input.id);
66
74
  const existing = this.rows.get(key);
67
75
  if (!existing)
@@ -0,0 +1,4 @@
1
+ import type { VaultIdentity, VaultOptions } from "./vault.types.js";
2
+ /** JSON tuple encoding prevents colons or separators in host identifiers aliasing another row. */
3
+ export declare function buildVaultAad(input: VaultIdentity, _options?: VaultOptions): string;
4
+ //# sourceMappingURL=vault.aad.d.ts.map
@@ -0,0 +1,8 @@
1
+ import { formatAad } from "../aad.js";
2
+ /** JSON tuple encoding prevents colons or separators in host identifiers aliasing another row. */
3
+ export function buildVaultAad(input, _options = {}) {
4
+ return formatAad({ kind: "secrets-vault", version: "v1", parts: [JSON.stringify([
5
+ input.workspaceId, input.id, input.kind, input.ownerKey, input.principalId, input.binding,
6
+ ])] });
7
+ }
8
+ //# sourceMappingURL=vault.aad.js.map
@@ -0,0 +1,10 @@
1
+ import { type VaultKind, type VaultMetaParsers, type VaultOptions } from "./vault.types.js";
2
+ /** Copy eligibility lives with the kind contracts; MCP rows require their server configuration. */
3
+ export declare const isCopyableKind: Readonly<Record<VaultKind, boolean>>;
4
+ /** Minimal structural parsers. Hosts inject catalog, URL/origin and safe-metadata policy here;
5
+ * secrets stay in payload JSON. Overrides own the complete shape, including merged patches.
6
+ */
7
+ export declare function createVaultMetaParsers(_required: VaultOptions, { parsers }?: {
8
+ parsers?: Partial<VaultMetaParsers>;
9
+ }): VaultMetaParsers;
10
+ //# sourceMappingURL=vault.meta.d.ts.map
@@ -0,0 +1,52 @@
1
+ import { VAULT_KINDS, VaultError } from "./vault.types.js";
2
+ /** Copy eligibility lives with the kind contracts; MCP rows require their server configuration. */
3
+ export const isCopyableKind = Object.freeze({
4
+ "site-assistant": true, "personal-execution": false, "media-provider": true, custom: true,
5
+ "publish-peer": true, "database-destination": true, vendor: true, "source-control": true,
6
+ "mcp-env": false, "mcp-auth": false,
7
+ });
8
+ const FIELDS = {
9
+ "site-assistant": { baseUrl: "string", model: "string" },
10
+ "personal-execution": { protocol: "string", baseUrl: "string", model: "string", maxTokens: "number" },
11
+ "media-provider": { baseUrl: "string", model: "string" },
12
+ custom: { category: "string", baseUrl: "string", additionalHosts: "strings", username: "string" },
13
+ "publish-peer": { baseUrl: "string", remoteWorkspaceId: "string" },
14
+ "database-destination": { description: "object", lastRun: "object" },
15
+ vendor: { accountLabel: "string" }, "source-control": { accountLabel: "string" },
16
+ "mcp-env": {}, "mcp-auth": {},
17
+ };
18
+ function isObject(value) {
19
+ return value !== null && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype;
20
+ }
21
+ function isJson(value) {
22
+ if (value === null || typeof value === "string" || typeof value === "boolean")
23
+ return true;
24
+ if (typeof value === "number")
25
+ return Number.isFinite(value);
26
+ if (Array.isArray(value))
27
+ return value.every(isJson);
28
+ return isObject(value) && Object.keys(value).every(key => !["__proto__", "constructor", "prototype"].includes(key) && isJson(value[key]));
29
+ }
30
+ /** Minimal structural parsers. Hosts inject catalog, URL/origin and safe-metadata policy here;
31
+ * secrets stay in payload JSON. Overrides own the complete shape, including merged patches.
32
+ */
33
+ export function createVaultMetaParsers(_required, { parsers = {} } = {}) {
34
+ return Object.fromEntries(VAULT_KINDS.map(kind => [kind, ({ meta }) => {
35
+ if (!isObject(meta) || !isJson(meta) || Object.keys(meta).some(key => !/^[A-Za-z0-9_]+$/.test(key)))
36
+ throw new VaultError({ code: "invalid-meta" });
37
+ if (parsers[kind]) {
38
+ const parsed = parsers[kind]({ meta: structuredClone(meta) });
39
+ if (!isObject(parsed) || !isJson(parsed) || Object.keys(parsed).some(key => !/^[A-Za-z0-9_]+$/.test(key)))
40
+ throw new VaultError({ code: "invalid-meta" });
41
+ return structuredClone(parsed);
42
+ }
43
+ for (const [key, value] of Object.entries(meta)) {
44
+ const field = Object.hasOwn(FIELDS[kind], key) ? FIELDS[kind][key] : undefined;
45
+ if (!field || (value !== null && !(field === "strings" ? Array.isArray(value) && value.every(v => typeof v === "string") :
46
+ field === "object" ? isObject(value) : typeof value === field)))
47
+ throw new VaultError({ code: "invalid-meta" });
48
+ }
49
+ return structuredClone(meta);
50
+ }]));
51
+ }
52
+ //# sourceMappingURL=vault.meta.js.map
@@ -0,0 +1,51 @@
1
+ import { type VaultBatchOp, type VaultBatchResult, type VaultDefaultQuery, type VaultSingletonQuery, type VaultMeta, type VaultOptions, type VaultQuery, type VaultRecord, type VaultRepoPort } from "./vault.types.js";
2
+ /** Single-turn mutations are atomic in JS; clones prevent callers bypassing CAS through aliases. */
3
+ export declare class InMemoryVaultRepo implements VaultRepoPort {
4
+ private rows;
5
+ private singletons;
6
+ constructor(_required: VaultOptions, _options?: VaultOptions);
7
+ private key;
8
+ find(input: {
9
+ workspaceId: string;
10
+ id: string;
11
+ }, _options?: VaultOptions): Promise<VaultRecord | null>;
12
+ private singletonKey;
13
+ /** Index all four optional scope combinations; copy sets so batch staging never mutates live indexes. */
14
+ private indexSingleton;
15
+ /** Indexed singleton selection; clone only the selected row, ignoring provider/default. */
16
+ findOne(input: VaultSingletonQuery, _options?: VaultOptions): Promise<VaultRecord | null>;
17
+ /** Stage through the same mutation owner, then publish once. No await can expose partial rows
18
+ * or let a concurrent writer be lost between snapshot and swap. O(n + ops*n) fixture work.
19
+ */
20
+ applyBatch({ ops }: {
21
+ ops: readonly VaultBatchOp[];
22
+ }, _options?: VaultOptions): Promise<VaultBatchResult[]>;
23
+ /** O(n) memory scan, n stored rows; clones only the default, never its siblings. */
24
+ findDefault(input: VaultDefaultQuery, _options?: VaultOptions): Promise<VaultRecord | null>;
25
+ list(input: VaultQuery, _options?: VaultOptions): Promise<VaultRecord[]>;
26
+ put(input: {
27
+ record: VaultRecord;
28
+ expectedRevision: number | null;
29
+ }, _options?: VaultOptions): Promise<VaultRecord>;
30
+ /** Synchronous mutation owner shared with batch staging; default moves never re-seal. */
31
+ private putRecord;
32
+ patchMeta(input: {
33
+ workspaceId: string;
34
+ id: string;
35
+ expectedRevision: number;
36
+ patch: VaultMeta;
37
+ updatedAt: string;
38
+ }, _options?: VaultOptions): Promise<VaultRecord>;
39
+ /** Clone merged metadata before mutating the staged/live map so failures leave it intact. */
40
+ private patchRecord;
41
+ delete(input: {
42
+ workspaceId: string;
43
+ id: string;
44
+ expectedRevision: number;
45
+ }, _options?: VaultOptions): Promise<boolean>;
46
+ /** Synchronous CAS delete shares promotion with provider moves and batch staging. */
47
+ private deleteRecord;
48
+ /** O(n) winner scan; promotion changes only default/revision, matching SQL. */
49
+ private promote;
50
+ }
51
+ //# sourceMappingURL=vault.repo.memory.d.ts.map
@@ -0,0 +1,148 @@
1
+ import { assertVaultReplacement, vaultGroupKey, VaultError } from "./vault.types.js";
2
+ /** Single-turn mutations are atomic in JS; clones prevent callers bypassing CAS through aliases. */
3
+ export class InMemoryVaultRepo {
4
+ rows = new Map();
5
+ singletons = new Map();
6
+ constructor(_required, _options = {}) { }
7
+ key(input) { return JSON.stringify([input.workspaceId, input.id]); }
8
+ async find(input, _options = {}) {
9
+ return structuredClone(this.rows.get(this.key(input)) ?? null);
10
+ }
11
+ singletonKey(input) {
12
+ return JSON.stringify([input.workspaceId, input.kind, input.ownerKey !== undefined, input.ownerKey ?? null,
13
+ input.principalId !== undefined, input.principalId ?? null]);
14
+ }
15
+ /** Index all four optional scope combinations; copy sets so batch staging never mutates live indexes. */
16
+ indexSingleton({ record, remove }, _options = {}) {
17
+ for (const ownerKey of [undefined, record.ownerKey])
18
+ for (const principalId of [undefined, record.principalId]) {
19
+ const scope = this.singletonKey({ workspaceId: record.workspaceId, kind: record.kind, ownerKey, principalId });
20
+ const ids = new Set(this.singletons.get(scope));
21
+ if (remove)
22
+ ids.delete(this.key(record));
23
+ else
24
+ ids.add(this.key(record));
25
+ if (ids.size)
26
+ this.singletons.set(scope, ids);
27
+ else
28
+ this.singletons.delete(scope);
29
+ }
30
+ }
31
+ /** Indexed singleton selection; clone only the selected row, ignoring provider/default. */
32
+ async findOne(input, _options = {}) {
33
+ const id = this.singletons.get(this.singletonKey(input))?.values().next().value;
34
+ return structuredClone(id === undefined ? null : this.rows.get(id));
35
+ }
36
+ /** Stage through the same mutation owner, then publish once. No await can expose partial rows
37
+ * or let a concurrent writer be lost between snapshot and swap. O(n + ops*n) fixture work.
38
+ */
39
+ async applyBatch({ ops }, _options = {}) {
40
+ const staged = new InMemoryVaultRepo({});
41
+ staged.rows = new Map(this.rows);
42
+ staged.singletons = new Map(this.singletons);
43
+ const results = ops.map(op => {
44
+ if ("put" in op)
45
+ return staged.putRecord(op.put);
46
+ if ("delete" in op)
47
+ return staged.deleteRecord(op.delete);
48
+ return staged.patchRecord(op.patchMeta);
49
+ });
50
+ this.rows = staged.rows;
51
+ this.singletons = staged.singletons;
52
+ return results;
53
+ }
54
+ /** O(n) memory scan, n stored rows; clones only the default, never its siblings. */
55
+ async findDefault(input, _options = {}) {
56
+ for (const row of this.rows.values()) {
57
+ if (row.isDefault && row.workspaceId === input.workspaceId && row.kind === input.kind && row.ownerKey === input.ownerKey &&
58
+ row.principalId === input.principalId && row.provider === input.provider)
59
+ return structuredClone(row);
60
+ }
61
+ return null;
62
+ }
63
+ async list(input, _options = {}) {
64
+ return structuredClone([...this.rows.values()].filter(row => row.workspaceId === input.workspaceId && row.kind === input.kind &&
65
+ row.ownerKey === input.ownerKey && row.principalId === input.principalId && (input.provider === undefined || row.provider === input.provider))
66
+ .sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
67
+ }
68
+ async put(input, _options = {}) {
69
+ return this.putRecord(input);
70
+ }
71
+ /** Synchronous mutation owner shared with batch staging; default moves never re-seal. */
72
+ putRecord(input, _options = {}) {
73
+ const { record } = input;
74
+ const key = this.key(record);
75
+ const existing = this.rows.get(key) ?? null;
76
+ assertVaultReplacement({ ...input, existing });
77
+ const groupKey = vaultGroupKey({ record });
78
+ let hasGroup = false;
79
+ for (const row of this.rows.values())
80
+ if (vaultGroupKey({ record: row }) === groupKey) {
81
+ hasGroup = true;
82
+ if (row.id !== record.id && row.label === record.label)
83
+ throw new VaultError({ code: "conflict" });
84
+ }
85
+ const saved = structuredClone({ ...record, isDefault: record.isDefault || !hasGroup });
86
+ // Clear first, matching a SQL partial UNIQUE index; no transient duplicate default is required.
87
+ if (saved.isDefault)
88
+ for (const row of this.rows.values()) {
89
+ if (row.id !== saved.id && row.isDefault && vaultGroupKey({ record: row }) === groupKey) {
90
+ this.rows.set(this.key(row), { ...row, isDefault: false, revision: row.revision + 1 });
91
+ }
92
+ }
93
+ this.rows.set(key, saved);
94
+ if (!existing)
95
+ this.indexSingleton({ record: saved, remove: false });
96
+ if (existing?.isDefault && vaultGroupKey({ record: existing }) !== groupKey)
97
+ this.promote({ record: existing });
98
+ return structuredClone(saved);
99
+ }
100
+ async patchMeta(input, _options = {}) {
101
+ return this.patchRecord(input);
102
+ }
103
+ /** Clone merged metadata before mutating the staged/live map so failures leave it intact. */
104
+ patchRecord(input, _options = {}) {
105
+ const key = this.key(input);
106
+ const existing = this.rows.get(key);
107
+ if (existing?.revision !== input.expectedRevision)
108
+ throw new VaultError({ code: "conflict" });
109
+ const saved = { ...existing, meta: structuredClone({ ...existing.meta, ...input.patch }), updatedAt: input.updatedAt, revision: existing.revision + 1 };
110
+ this.rows.set(key, saved);
111
+ return structuredClone(saved);
112
+ }
113
+ async delete(input, _options = {}) {
114
+ return this.deleteRecord(input);
115
+ }
116
+ /** Synchronous CAS delete shares promotion with provider moves and batch staging. */
117
+ deleteRecord(input, _options = {}) {
118
+ const key = this.key(input);
119
+ const existing = this.rows.get(key);
120
+ if (!existing)
121
+ return false;
122
+ if (existing.revision !== input.expectedRevision)
123
+ throw new VaultError({ code: "conflict" });
124
+ this.rows.delete(key);
125
+ this.indexSingleton({ record: existing, remove: true });
126
+ if (existing.isDefault)
127
+ this.promote({ record: existing });
128
+ return true;
129
+ }
130
+ /** O(n) winner scan; promotion changes only default/revision, matching SQL. */
131
+ promote({ record }, _options = {}) {
132
+ const groupKey = vaultGroupKey({ record });
133
+ let promoted;
134
+ // SQL BINARY/C compares UTF-8 bytes; JS's UTF-16 order differs for supplementary IDs.
135
+ for (const row of this.rows.values()) {
136
+ if (vaultGroupKey({ record: row }) === groupKey && (!promoted || row.updatedAt > promoted.updatedAt ||
137
+ (row.updatedAt === promoted.updatedAt && Buffer.compare(Buffer.from(row.id), Buffer.from(promoted.id)) > 0))) {
138
+ promoted = { id: row.id, updatedAt: row.updatedAt };
139
+ }
140
+ }
141
+ if (promoted) {
142
+ const promotedKey = this.key({ workspaceId: record.workspaceId, id: promoted.id });
143
+ const row = this.rows.get(promotedKey);
144
+ this.rows.set(promotedKey, { ...row, isDefault: true, revision: row.revision + 1 });
145
+ }
146
+ }
147
+ }
148
+ //# sourceMappingURL=vault.repo.memory.js.map