@oddington/sdk 0.1.4 → 0.1.5

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 CHANGED
@@ -42,23 +42,71 @@ const report = await files.create({
42
42
  const descriptor = await files.get(report.id);
43
43
  const response = await files.read(descriptor.id);
44
44
  await response.text(); // Also supports blob(), arrayBuffer() and body streaming.
45
+ const reading = await files.enrich({ file: report.id, name: 'read.md', content: readingMarkdown });
46
+ await files.enrich({ file: report.id, name: 'extracted.json', content: JSON.stringify(extractedValue) });
47
+ const current = await files.get(report.id); // Discover retained enrichment descriptors.
48
+ const context = await files.read(current.id, ['read.md', 'extracted.json']);
49
+ await context['read.md'].text();
50
+ await context['extracted.json'].json(); // Each body is independently consumed.
51
+ await files.enrich({ file: report.id, name: 'read.md', content: revisedMarkdown, replace: reading.sha256 });
52
+ await files.upload(report.id); // Move a local File and every retained enrichment to remote storage.
45
53
  ```
46
54
 
47
- Hosted Files retains remote originals only, with empty `enrichments`. It rejects
48
- `local`, `both` and explicit routine local preparation with `unsupported_context`.
55
+ Hosted Files retains remote originals and caller-produced named enrichments;
56
+ `storage: 'local'` requires a managed CLI parent. Configure that parent with
57
+ `ODDINGTON_FILES_ROOT` pointing to an empty or already OS-managed dedicated
58
+ directory outside tracked Git content. `ODDINGTON_FILES_DEFAULT=local|remote`
59
+ selects its default; without it, remote remains the default. Per-File `storage`
60
+ overrides it. The root stays private to the parent. Local creation without a
61
+ configured root fails with `unsupported_context`. Files have one durable home;
62
+ `both` is invalid. Local retention establishes no hosted registration.
63
+ Fresh runs recover the same creator's File by its request key, and verify
64
+ retained bytes before returning a descriptor or content. A dead parent fails
65
+ closed. `os routine prepare` seals checked local File references and defers
66
+ new remote Files, their enrichments and explicit uploads until `os routine publish`; legacy Inbox
67
+ file preparation remains additive. Existing hosted Files are read-only during
68
+ preparation.
69
+ `upload(id)` preserves the File descriptor, creator access and existing Source
70
+ references. Local bytes remain authoritative until all remote objects verify;
71
+ retrying a completed upload returns the same File. Later reads and enrichments
72
+ follow remote storage; any remaining managed local bytes are disposable cache.
73
+ An unreferenced local File is disclosed remotely only when explicitly uploaded
74
+ or successfully published as a Source. An authorized hosted caller receives
75
+ non-retryable `local_only` (409) until upload succeeds; unrelated callers receive
76
+ `access_denied`.
49
77
  Unnamed inputs require a safe `name`; streams also require declared `size`.
50
78
  Blob and byte-array declarations must match their intrinsic size. Originals are
51
- bounded to 16 MiB, metadata to 4 KiB, and descriptor/error bodies to 16 KiB.
52
- Creates allow four concurrent buffered operations and a 30-second deadline;
79
+ bounded to 16 MiB per object, metadata to 4 KiB, and descriptor/error bodies to 16 KiB.
80
+ Operations share four concurrent slots and a 30-second deadline;
53
81
  transport retries at most once with the same prepared bytes and request key.
54
82
  Omitting a key generates one for that invocation. Supply a stable key to recover
55
83
  across processes or fresh routine runs. Caller inputs remain owned by the caller.
56
- Each request resolves current authority; routine Files calls go directly to the
57
- selected workspace's HTTPS OS host. `FileOperationError` exposes a bounded code,
84
+ Each request resolves current authority; hosted routine Files calls use the
85
+ selected workspace's HTTPS OS host, and managed calls use the authenticated
86
+ private parent transport. `FileOperationError` exposes a bounded code,
58
87
  status and retryability without server details. Reads return the successful Web
59
- Response without consuming its body. Creation grants private creator access;
88
+ Response without consuming its body. Local transfer is bounded and buffered
89
+ before constructing that Response. Creation grants private creator access;
60
90
  workspace membership alone never grants another creator's File.
61
91
 
92
+ `enrich` accepts a string encoded as UTF-8 or exact `Uint8Array` bytes, a safe
93
+ flat filename, and an optional MIME hint. OS verifies text/Markdown/JSON or
94
+ reduces the content to generic binary metadata; it performs no extraction or
95
+ model work. The first enrichment atomically promotes the storage bundle while
96
+ preserving original bytes, File identity, name, path and access. An identical
97
+ retry returns the retained descriptor. Different content conflicts unless
98
+ `replace` matches the current enrichment hash; repeating a completed replacement
99
+ still succeeds with its preceding hash. There is no enrichment request key.
100
+
101
+ `read(id)` returns the original; `read(id, name)` returns one enrichment.
102
+ `read(id, names)` validates at most 16 distinct safe names, deduplicates repeats,
103
+ and returns an object of unconsumed Responses after all reads succeed. An empty
104
+ list returns an empty object in a valid runtime context. Missing authorized names
105
+ return `invalid_input`; a failed keyed request cancels obtained bodies. Each name
106
+ is read independently with fresh authority; there is no cross-name revision snapshot.
107
+ Names cannot contain separators or use the reserved `original` basename with
108
+ or without an extension. Enrichments share their parent File's access and retention.
109
+
62
110
  Read a named repository through the authenticated host context:
63
111
 
64
112
  ```ts
@@ -81,7 +129,7 @@ Binding is inert. Each operation resolves current workspace, endpoint and
81
129
  short-lived authority from the trusted app host or managed routine context.
82
130
  Customer calls cannot configure those values. Missing or expired context
83
131
  rejects; a named handle does not expand repository access. During explicit
84
- `os routine run --local`, the same calls use the matched checkout's committed
132
+ `os routine prepare`, the same calls use the matched checkout's committed
85
133
  version through a private parent transport. Dirty files are ignored. An OS
86
134
  cache outside the checkout reuses derived data; reads require no remote access
87
135
  or preparation storage. Later accepted record effects advance the run's view
@@ -120,21 +168,37 @@ eligibility. Repeat the same bounds when continuing a cursor.
120
168
  For reads, only `capacity` and `unavailable` are retryable; an error never includes private
121
169
  server details. Server boundaries own query bounds and authorization.
122
170
 
123
- Guarded updates use the same handle in hosted and explicitly local routines:
171
+ Schema-driven creates, guarded updates and guarded deletions use the same handle
172
+ in immediate and prepared routines:
124
173
 
125
174
  ```ts
126
175
  const changes = general.changes();
176
+ changes.create({
177
+ $ref: 'invoices/INV-124', $filing: { year: '2026' },
178
+ type: 'invoice', number: 'INV-124', status: 'draft', amount: 1200,
179
+ });
127
180
  changes.update(first.invoices[0], { amount: 30 });
181
+ changes.delete(first.invoices[1]);
128
182
  await changes.validate();
129
183
  const publication = await changes.publish();
130
184
  ```
131
185
 
132
- Preparation captures only the Record's `$ref`, `$version` and explicit set,
133
- without I/O. Validation checks the complete committed result without effects;
134
- publication repeats guarded validation and applies the whole update batch.
135
- Success returns `{ version, created: [], updated: [{ $ref, $version }], deleted: [] }`
186
+ Creation captures a complete Record and stable `$ref`, without I/O or a caller
187
+ path. OS resolves exactly one collection and its schema-declared filing path.
188
+ Optional `$filing` supplies only unresolved named safe components and is never
189
+ stored or returned as Record data; missing components reject by name. Updates
190
+ capture only `$ref`, `$version` and explicit fields. Deletion captures only the
191
+ selected `$ref` and `$version`. Links within the deletion set are allowed;
192
+ schema-declared links from any committed Record outside it refuse the whole group,
193
+ even if a proposed update removes that link. Validation checks the complete
194
+ candidate graph without effects; publication repeats authoritative validation
195
+ and applies every create/update/delete in one commit.
196
+ Success returns `{ version, created: [{ $ref, $version }], updated: [{ $ref, $version }], deleted: [{ $ref, $version }] }`
136
197
  only after its exact index is ready. Later current reads discover that version.
137
- During `os routine run --local`, publication retains the exact proposal and
198
+ Deleted identities keep their captured versions; deletion creates no new Record
199
+ version or tombstone. A Source deletion removes only that guarded Record, retaining
200
+ its File, enrichments and existing durable uses.
201
+ During `os routine prepare`, publication retains the exact proposal and
138
202
  advances only run-local reads. `os routine publish` later applies and indexes
139
203
  that immutable proposal; unrelated staged and unstaged work is preserved.
140
204
 
@@ -143,8 +207,10 @@ that immutable proposal; unrelated staged and unstaged work is preserved.
143
207
  retry the same change set to reconcile its retained identity. A transport failure
144
208
  also keeps that identity. Concurrent publication calls coalesce, confirmed calls
145
209
  replay the result, and changed intent requires a new change set. Updates are
146
- bounded to 10,000 Records and the 256 KiB Data request limit. Create, delete,
147
- `$filing`, search and history are outside this supported Changes slice.
210
+ bounded with creates and deletions to 10,000 total operations and the 256 KiB Data
211
+ request limit, with one operation per ref. Search and history are outside this
212
+ Changes slice. Source Records use ordinary schema rules; Data also validates OS
213
+ File identity/access and registers durable uses from its exact ready artifact.
148
214
 
149
215
  Bind a declared Airtable connection with `connections.airtable('project-management')`
150
216
  from the package root. Import and binding perform no network or credential work.
@@ -182,8 +248,14 @@ The defaults read `ODDINGTON_ROUTINE_WORKSPACE`, `ODDINGTON_ROUTINE_RUN` and
182
248
  `ODDINGTON_INBOX_WORKING_ROOT` (or the current directory). Callers may supply the
183
249
  existing `workspace`, `capability` and `workingRoot` options. Runtime context
184
250
  provides authority; callers cannot select repository remotes or expand grants.
185
- The same calls prepare locally when launched with `os routine run --local`, and
186
- the CLI owns explicit publication and recovery of that prepared run.
251
+ `os routine run` publishes as capabilities are called. The same calls prepare
252
+ work when launched with `os routine prepare`, and `os routine publish` owns
253
+ explicit publication and recovery of that prepared run. Preparation may contact
254
+ OS for authentication and reads, and declared connections may contact their
255
+ providers; it does not promise offline execution. Execution location and File
256
+ storage are independent of the publication lifecycle. `os routine run --local`
257
+ remains a deprecated compatibility alias for `os routine prepare` and prints
258
+ the replacement command.
187
259
 
188
260
  Retain evidence with `inbox.files.add({ path: 'evidence.txt' })`. Paths stay within
189
261
  the invocation's working root and uploaded bytes are verified against the retained
package/dist/index.js CHANGED
@@ -5985,9 +5985,9 @@ var init_errors = __esm(() => {
5985
5985
  }(Error);
5986
5986
  CorruptPageTreeError = function(_super) {
5987
5987
  __extends(CorruptPageTreeError2, _super);
5988
- function CorruptPageTreeError2(targetIndex, operation) {
5988
+ function CorruptPageTreeError2(targetIndex, operation2) {
5989
5989
  var _this = this;
5990
- var msg = "Failed to " + operation + " at targetIndex=" + targetIndex + " due to corrupt page tree: It is likely that one or more 'Count' entries are invalid";
5990
+ var msg = "Failed to " + operation2 + " at targetIndex=" + targetIndex + " due to corrupt page tree: It is likely that one or more 'Count' entries are invalid";
5991
5991
  _this = _super.call(this, msg) || this;
5992
5992
  return _this;
5993
5993
  }
@@ -21329,9 +21329,22 @@ function validateRecordDelete(value) {
21329
21329
  }
21330
21330
  function validateRecordChange(value) {
21331
21331
  const input = serializable(value);
21332
- if (Array.isArray(input?.records) && input.records.length && input.records.every((record) => !!record && typeof record === "object" && !Array.isArray(record) && !("set" in record) && !("fields" in record)))
21333
- return validateRecordDelete(input);
21334
- return validateRecordWrite(input);
21332
+ if (!input || Array.isArray(input) || typeof input !== "object" || Object.keys(input).sort().join(",") !== "records,repository" || !validRepositoryAlias(input.repository) || !Array.isArray(input.records) || !input.records.length || input.records.length > 1e4 || new Set(input.records.map((record) => record?.ref)).size !== input.records.length || new TextEncoder().encode(JSON.stringify(input)).byteLength > RECORD_WRITE_INPUT_LIMIT)
21333
+ throw new InboxContractError("Invalid record change batch");
21334
+ const writes = [], deletes = [];
21335
+ for (const record of input.records) {
21336
+ if (!record || typeof record !== "object" || Array.isArray(record))
21337
+ throw new InboxContractError("Invalid record change");
21338
+ if ("set" in record || "fields" in record)
21339
+ writes.push(record);
21340
+ else
21341
+ deletes.push(record);
21342
+ }
21343
+ if (writes.length)
21344
+ validateRecordWrite({ repository: input.repository, records: writes });
21345
+ if (deletes.length)
21346
+ validateRecordDelete({ repository: input.repository, records: deletes });
21347
+ return structuredClone(input);
21335
21348
  }
21336
21349
  function validateRoutineRecordWrite(value) {
21337
21350
  const body = serializable(value);
@@ -21685,11 +21698,13 @@ class DataOperationError extends Error {
21685
21698
  code;
21686
21699
  retryable;
21687
21700
  effect;
21688
- constructor(code, retryable = false, effect) {
21701
+ component;
21702
+ constructor(code, retryable = false, effect, component) {
21689
21703
  super(`Data operation failed: ${code}`);
21690
21704
  this.code = code;
21691
21705
  this.retryable = retryable;
21692
21706
  this.effect = effect;
21707
+ this.component = component;
21693
21708
  this.name = "DataOperationError";
21694
21709
  }
21695
21710
  }
@@ -21727,14 +21742,20 @@ function captureValue(value, ancestors = new Set, depth = 0) {
21727
21742
  ancestors.delete(value);
21728
21743
  }
21729
21744
  }
21745
+ function captureDataDelete(record2) {
21746
+ const identity = mapping(record2);
21747
+ if (!validRecordRef(identity.$ref) || typeof identity.$version !== "string" || !identity.$version.length || identity.$version.length > 512)
21748
+ return invalidChange();
21749
+ return { ref: identity.$ref, expectedVersion: identity.$version };
21750
+ }
21730
21751
  function captureDataUpdate(record2, set) {
21731
- const identity = mapping(record2), fields3 = mapping(set);
21732
- if (!validRecordRef(identity.$ref) || typeof identity.$version !== "string" || !identity.$version.length || identity.$version.length > 512 || !Object.keys(fields3).length || Object.keys(fields3).some((key) => key.startsWith("$") || ["id", "type"].includes(key)))
21752
+ const identity = captureDataDelete(record2), fields3 = mapping(set);
21753
+ if (!Object.keys(fields3).length || Object.keys(fields3).some((key) => key.startsWith("$") || ["id", "type"].includes(key)))
21733
21754
  return invalidChange();
21734
- return { ref: identity.$ref, expectedVersion: identity.$version, set: captureValue(fields3) };
21755
+ return { ...identity, set: captureValue(fields3) };
21735
21756
  }
21736
- function captureDataUpdates(value) {
21737
- if (!Array.isArray(value) || !value.length || value.length > 1e4)
21757
+ function captureDataUpdates(value, allowEmpty = false) {
21758
+ if (!Array.isArray(value) || !allowEmpty && !value.length || value.length > 1e4)
21738
21759
  return invalidChange();
21739
21760
  const updates = value.map((item) => {
21740
21761
  const row = mapping(item);
@@ -21746,6 +21767,46 @@ function captureDataUpdates(value) {
21746
21767
  return invalidChange();
21747
21768
  return updates;
21748
21769
  }
21770
+ function captureDataCreate(value) {
21771
+ const row = mapping(value);
21772
+ if (!validRecordRef(row.$ref) || Object.keys(row).some((key) => key.startsWith("$") && !["$ref", "$filing"].includes(key)))
21773
+ return invalidChange();
21774
+ const { $ref: ref, $filing, ...fields3 } = row;
21775
+ let filing;
21776
+ if ($filing !== undefined) {
21777
+ const components = mapping($filing);
21778
+ if (Object.entries(components).some(([name, value2]) => !/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name) || ["constructor", "prototype"].includes(name) || typeof value2 !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(value2)))
21779
+ return invalidChange();
21780
+ filing = { ...components };
21781
+ }
21782
+ return { ref, fields: captureValue(fields3), ...filing ? { filing } : {} };
21783
+ }
21784
+ function captureDataChanges(updatesValue, createsValue = [], deletesValue = []) {
21785
+ const updates = captureDataUpdates(updatesValue, true);
21786
+ if (!Array.isArray(createsValue) || createsValue.length > 1e4)
21787
+ return invalidChange();
21788
+ const creates = createsValue.map((item) => {
21789
+ const row = mapping(item);
21790
+ if (Object.keys(row).some((key) => !["ref", "fields", "filing"].includes(key)) || !Object.hasOwn(row, "ref") || !Object.hasOwn(row, "fields"))
21791
+ return invalidChange();
21792
+ const fields3 = mapping(row.fields);
21793
+ if (Object.keys(fields3).some((key) => key.startsWith("$")))
21794
+ return invalidChange();
21795
+ return captureDataCreate({ ...fields3, $ref: row.ref, ...Object.hasOwn(row, "filing") ? { $filing: row.filing } : {} });
21796
+ });
21797
+ if (!Array.isArray(deletesValue) || deletesValue.length > 1e4)
21798
+ return invalidChange();
21799
+ const deletes = deletesValue.map((item) => {
21800
+ const row = mapping(item);
21801
+ if (Object.keys(row).sort().join(",") !== "expectedVersion,ref")
21802
+ return invalidChange();
21803
+ return captureDataDelete({ $ref: row.ref, $version: row.expectedVersion });
21804
+ });
21805
+ const refs = [...updates, ...creates, ...deletes].map((item) => item.ref);
21806
+ if (!refs.length || refs.length > 1e4 || new Set(refs).size !== refs.length)
21807
+ return invalidChange();
21808
+ return { updates, creates, deletes };
21809
+ }
21749
21810
  function checkDataRequestSize(input) {
21750
21811
  if (new TextEncoder().encode(JSON.stringify(input)).byteLength > DATA_REQUEST_LIMIT)
21751
21812
  throw new DataOperationError("request_too_large", false, "unapplied");
@@ -21764,7 +21825,7 @@ function dataTransportError(status, value) {
21764
21825
  const row = error && typeof error === "object" ? error : undefined;
21765
21826
  const code = failures[status]?.find((allowed) => allowed === row?.code) ?? "unavailable";
21766
21827
  const effect = ["unapplied", "uncertain", "applied"].includes(String(row?.effect)) ? row.effect : undefined;
21767
- return new DataOperationError(code, ["capacity", "unavailable", "publication_pending"].includes(code), code === "conflict" ? "unapplied" : effect);
21828
+ return new DataOperationError(code, ["capacity", "unavailable", "publication_pending"].includes(code), code === "conflict" ? "unapplied" : effect, code === "invalid_change" && typeof row?.component === "string" && /^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/.test(row.component) ? row.component : undefined);
21768
21829
  }
21769
21830
 
21770
21831
  // ../../shared/files-contract.ts
@@ -21787,7 +21848,7 @@ function parseFileDescriptor(value) {
21787
21848
  if (!value || typeof value !== "object" || Array.isArray(value))
21788
21849
  return invalid();
21789
21850
  const f = value;
21790
- if (Object.keys(f).sort().join(",") !== "details,enrichments,id,mediaType,name,path,sha256,size" || typeof f.id !== "string" || !FILE_ID_PATTERN.test(f.id) || !safeFileName(f.name) || !safeFilePath(f.path) || !Number.isSafeInteger(f.size) || f.size < 0 || typeof f.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(f.sha256) || !f.enrichments || typeof f.enrichments !== "object" || Array.isArray(f.enrichments) || Object.keys(f.enrichments).length !== 0 || !f.details || typeof f.details !== "object" || Array.isArray(f.details))
21851
+ if (Object.keys(f).sort().join(",") !== "details,enrichments,id,mediaType,name,path,sha256,size" || typeof f.id !== "string" || !FILE_ID_PATTERN.test(f.id) || !safeFileName(f.name) || !safeFilePath(f.path) || !Number.isSafeInteger(f.size) || f.size < 0 || f.size > 16 * 1024 * 1024 || typeof f.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(f.sha256) || !f.enrichments || typeof f.enrichments !== "object" || Array.isArray(f.enrichments) || !f.details || typeof f.details !== "object" || Array.isArray(f.details))
21791
21852
  return invalid();
21792
21853
  const d = f.details;
21793
21854
  if (d.kind === "other") {
@@ -21798,8 +21859,42 @@ function parseFileDescriptor(value) {
21798
21859
  return invalid();
21799
21860
  } else
21800
21861
  return invalid();
21862
+ for (const [name, descriptor] of Object.entries(f.enrichments)) {
21863
+ if (!safeEnrichmentName(name))
21864
+ return invalid();
21865
+ parseEnrichmentDescriptor(descriptor);
21866
+ }
21867
+ if (new TextEncoder().encode(JSON.stringify(f)).byteLength > 16 * 1024)
21868
+ return invalid();
21801
21869
  return f;
21802
21870
  }
21871
+ function parseEnrichmentDescriptor(value) {
21872
+ const invalid = () => {
21873
+ throw new FileOperationError("unavailable", 503, true);
21874
+ };
21875
+ if (!value || typeof value !== "object" || Array.isArray(value))
21876
+ return invalid();
21877
+ const d = value;
21878
+ if (Object.keys(d).sort().join(",") !== "mediaType,sha256,size" || !["application/octet-stream", "text/plain", "text/markdown", "application/json"].includes(d.mediaType) || !Number.isSafeInteger(d.size) || d.size < 0 || d.size > 16 * 1024 * 1024 || typeof d.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(d.sha256))
21879
+ return invalid();
21880
+ return d;
21881
+ }
21882
+ function safeEnrichmentName(value) {
21883
+ return safeFileName(value) && !/^original(?:\.|$)/i.test(value);
21884
+ }
21885
+ function parseFileEnrichMetadata(value, maxBytes) {
21886
+ const invalid = () => {
21887
+ throw new FileOperationError("invalid_input", 400);
21888
+ };
21889
+ if (!value || typeof value !== "object" || Array.isArray(value))
21890
+ return invalid();
21891
+ const d = value;
21892
+ if (Object.keys(d).some((key) => !["name", "size", "mediaType", "replace"].includes(key)) || !safeEnrichmentName(d.name) || !Number.isSafeInteger(d.size) || d.size < 0 || d.mediaType !== undefined && (typeof d.mediaType !== "string" || d.mediaType.length > 255 || /[\u0000-\u001f\u007f]/.test(d.mediaType)) || d.replace !== undefined && (typeof d.replace !== "string" || !/^[0-9a-f]{64}$/.test(d.replace)))
21893
+ return invalid();
21894
+ if (d.size > maxBytes)
21895
+ throw new FileOperationError("request_too_large", 413);
21896
+ return d;
21897
+ }
21803
21898
  function safeFileName(value) {
21804
21899
  return typeof value === "string" && value.length > 0 && value.length <= 255 && value === value.trim() && value !== "." && value !== ".." && !/[\u0000-\u001f\u007f-\u009f/\\:*?"<>|]/u.test(value) && !/[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/.test(value) && !/[. ]$/.test(value) && !/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(value);
21805
21900
  }
@@ -21818,9 +21913,7 @@ function parseFileCreateMetadata(value, maxBytes) {
21818
21913
  const path = input.path === undefined ? "Files" : input.path;
21819
21914
  if (!safeFileName(input.name) || !safeFilePath(path))
21820
21915
  return invalid();
21821
- if (input.storage === "local" || input.storage === "both")
21822
- throw new FileOperationError("unsupported_context", 400);
21823
- if (input.storage !== undefined && input.storage !== "remote")
21916
+ if (input.storage !== undefined && input.storage !== "remote" && input.storage !== "local")
21824
21917
  return invalid();
21825
21918
  if (!Number.isSafeInteger(input.size) || input.size < 0)
21826
21919
  return invalid();
@@ -21842,7 +21935,14 @@ function parseFileCreateMetadata(value, maxBytes) {
21842
21935
  return invalid();
21843
21936
  csv = { encoding, delimiter, headerRow };
21844
21937
  }
21845
- return { name: input.name, path, storage: "remote", ...csv ? { csv } : {}, size: input.size, requestKey: input.requestKey };
21938
+ return {
21939
+ name: input.name,
21940
+ path,
21941
+ ...input.storage ? { storage: input.storage } : {},
21942
+ ...csv ? { csv } : {},
21943
+ size: input.size,
21944
+ requestKey: input.requestKey
21945
+ };
21846
21946
  }
21847
21947
 
21848
21948
  // ../../shared/os-origin.ts
@@ -21856,6 +21956,21 @@ function oddingtonOrigin(environment = process.env) {
21856
21956
  }
21857
21957
 
21858
21958
  // src/routine-local-transport.ts
21959
+ function privateRoutineRequest(environment = typeof process === "undefined" ? {} : process.env) {
21960
+ const prepared = ["ODDINGTON_ROUTINE_LOCAL", "ODDINGTON_ROUTINE_LOCAL_SOCKET", "ODDINGTON_ROUTINE_LOCAL_TOKEN"].some((key) => environment[key] !== undefined);
21961
+ const managed = ["ODDINGTON_FILES_SOCKET", "ODDINGTON_FILES_TOKEN"].some((key) => environment[key] !== undefined);
21962
+ if (!prepared && !managed)
21963
+ return;
21964
+ const socket = prepared ? environment.ODDINGTON_ROUTINE_LOCAL_SOCKET : environment.ODDINGTON_FILES_SOCKET;
21965
+ const token = prepared ? environment.ODDINGTON_ROUTINE_LOCAL_TOKEN : environment.ODDINGTON_FILES_TOKEN;
21966
+ const request = localRoutineRequest(prepared ? environment : {
21967
+ ODDINGTON_ROUTINE_LOCAL: "1",
21968
+ ODDINGTON_ROUTINE_LOCAL_SOCKET: socket,
21969
+ ODDINGTON_ROUTINE_LOCAL_TOKEN: token
21970
+ });
21971
+ const isCurrent = () => prepared ? environment.ODDINGTON_ROUTINE_LOCAL === "1" && environment.ODDINGTON_ROUTINE_LOCAL_SOCKET === socket && environment.ODDINGTON_ROUTINE_LOCAL_TOKEN === token : environment.ODDINGTON_FILES_SOCKET === socket && environment.ODDINGTON_FILES_TOKEN === token;
21972
+ return { request, isCurrent, identity: `private:${socket}` };
21973
+ }
21859
21974
  function localRoutineRequest(environment = typeof process === "undefined" ? {} : process.env) {
21860
21975
  const {
21861
21976
  ODDINGTON_ROUTINE_LOCAL: mode,
@@ -21865,11 +21980,11 @@ function localRoutineRequest(environment = typeof process === "undefined" ? {} :
21865
21980
  if (mode === undefined && socket === undefined && token === undefined)
21866
21981
  return;
21867
21982
  if (mode !== "1" || !socket || !token || !/^[0-9a-f]{64}$/.test(token))
21868
- throw new Error("Local preparation transport unavailable; nothing can publish");
21983
+ throw new Error("Preparation transport unavailable; nothing can publish");
21869
21984
  const tcp = /^tcp:\/\/127\.0\.0\.1:([1-9][0-9]{0,4})$/.exec(socket);
21870
21985
  const endpoint = tcp && tcp[0] === socket && Number(tcp[1]) <= 65535 ? { hostname: "127.0.0.1", port: Number(tcp[1]) } : socket.startsWith("/") && !socket.startsWith("//") && !socket.includes("\x00") ? { socketPath: socket } : undefined;
21871
21986
  if (!endpoint)
21872
- throw new Error("Local preparation transport unavailable; nothing can publish");
21987
+ throw new Error("Preparation transport unavailable; nothing can publish");
21873
21988
  return async (operation, input) => {
21874
21989
  const { request } = await import("node:http");
21875
21990
  return await new Promise((resolve, reject) => {
@@ -21880,7 +21995,7 @@ function localRoutineRequest(environment = typeof process === "undefined" ? {} :
21880
21995
  headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
21881
21996
  timeout: 120000
21882
21997
  }, (response) => {
21883
- const chunks = [];
21998
+ const bytes = Buffer.alloc(33554432);
21884
21999
  let size = 0;
21885
22000
  response.on("data", (chunk) => {
21886
22001
  size += chunk.length;
@@ -21888,17 +22003,37 @@ function localRoutineRequest(environment = typeof process === "undefined" ? {} :
21888
22003
  response.destroy();
21889
22004
  reject(new Error("Local result exceeds supported size"));
21890
22005
  } else
21891
- chunks.push(chunk);
22006
+ bytes.set(chunk, size - chunk.length);
21892
22007
  });
21893
22008
  response.on("error", reject);
21894
22009
  response.on("end", () => {
21895
22010
  try {
21896
- const result = JSON.parse(Buffer.concat(chunks).toString());
22011
+ const result = JSON.parse(bytes.subarray(0, size).toString());
21897
22012
  if (response.statusCode !== 200) {
22013
+ if (operation === "files/request") {
22014
+ const error = result.error;
22015
+ const status = response.statusCode ?? 503;
22016
+ const codes = [
22017
+ "invalid_input",
22018
+ "unsupported_context",
22019
+ "unauthenticated",
22020
+ "access_denied",
22021
+ "request_too_large",
22022
+ "conflict",
22023
+ "local_only",
22024
+ "integrity",
22025
+ "unavailable",
22026
+ "capacity",
22027
+ "method_not_allowed"
22028
+ ];
22029
+ if (error?.code && codes.includes(error.code))
22030
+ throw new FileOperationError(error.code, status, error.code === "capacity" || error.code === "unavailable");
22031
+ throw new FileOperationError("unavailable", 503, true);
22032
+ }
21898
22033
  if (operation === "data/query") {
21899
22034
  throw dataTransportError(response.statusCode ?? 0, result);
21900
22035
  }
21901
- throw new Error(typeof result.error === "string" ? result.error : "Local preparation failed");
22036
+ throw new Error(typeof result.error === "string" ? result.error : "Preparation failed");
21902
22037
  }
21903
22038
  resolve(result);
21904
22039
  } catch (error) {
@@ -21906,8 +22041,8 @@ function localRoutineRequest(environment = typeof process === "undefined" ? {} :
21906
22041
  }
21907
22042
  });
21908
22043
  });
21909
- call.on("error", () => reject(new Error("Local preparation transport unavailable; nothing was published")));
21910
- call.on("timeout", () => call.destroy(new Error("Local preparation timed out")));
22044
+ call.on("error", () => reject(new Error("Preparation transport unavailable; nothing was published")));
22045
+ call.on("timeout", () => call.destroy(new Error("Preparation timed out")));
21911
22046
  call.end(JSON.stringify(input));
21912
22047
  });
21913
22048
  };
@@ -21927,6 +22062,9 @@ async function resolveDataContext() {
21927
22062
  const local = localRoutineRequest();
21928
22063
  if (local)
21929
22064
  return { local };
22065
+ const managed = privateRoutineRequest();
22066
+ if (managed)
22067
+ return { local: managed.request };
21930
22068
  const host = memberContext;
21931
22069
  if (host !== undefined) {
21932
22070
  const context = await host();
@@ -21945,8 +22083,14 @@ async function resolveDataContext() {
21945
22083
  async function resolveFilesContext() {
21946
22084
  try {
21947
22085
  const environment = typeof process === "undefined" ? {} : process.env;
21948
- if (["ODDINGTON_ROUTINE_LOCAL", "ODDINGTON_ROUTINE_LOCAL_SOCKET", "ODDINGTON_ROUTINE_LOCAL_TOKEN"].some((key) => environment[key] !== undefined))
21949
- throw new Error;
22086
+ const local = privateRoutineRequest(environment);
22087
+ if (local)
22088
+ return {
22089
+ url: local.identity,
22090
+ headers: {},
22091
+ isCurrent: local.isCurrent,
22092
+ local: local.request
22093
+ };
21950
22094
  const host = memberContext;
21951
22095
  if (host) {
21952
22096
  const context = await host();
@@ -21954,13 +22098,14 @@ async function resolveFilesContext() {
21954
22098
  throw new Error;
21955
22099
  return {
21956
22100
  ...filesContext(context.workspace, context.endpoint, { authorization: `Bearer ${context.accessToken}` }),
21957
- isCurrent: context.isCurrent
22101
+ isCurrent: context.isCurrent,
22102
+ local: undefined
21958
22103
  };
21959
22104
  }
21960
22105
  if (!/^[0-9a-f]{64}$/.test(environment.ODDINGTON_ROUTINE_RUN ?? ""))
21961
22106
  throw new Error;
21962
22107
  const workspace = environment.ODDINGTON_ROUTINE_WORKSPACE;
21963
- return filesContext(workspace, `https://${workspace}.oddington.ai`, { "x-oddington-run": environment.ODDINGTON_ROUTINE_RUN });
22108
+ return { ...filesContext(workspace, `https://${workspace}.oddington.ai`, { "x-oddington-run": environment.ODDINGTON_ROUTINE_RUN }), local: undefined };
21964
22109
  } catch {
21965
22110
  throw new FileOperationError("unsupported_context", 400);
21966
22111
  }
@@ -22013,34 +22158,71 @@ var data = {
22013
22158
  },
22014
22159
  records: { select: (query) => send({ operation: "records.select", query }) },
22015
22160
  changes() {
22016
- let updates = [], publicationId;
22161
+ let updates = [], creates = [], deletes = [], publicationId;
22017
22162
  let pending, confirmed;
22018
22163
  const snapshot = () => {
22019
- const captured = captureDataUpdates(updates);
22164
+ const captured = captureDataChanges(updates, creates, deletes);
22165
+ const input = {
22166
+ updates: captured.updates,
22167
+ ...captured.creates.length ? { creates: captured.creates } : {},
22168
+ ...captured.deletes.length ? { deletes: captured.deletes } : {}
22169
+ };
22020
22170
  checkDataRequestSize({
22021
22171
  repository,
22022
22172
  operation: "changes.publish",
22023
- updates: captured,
22173
+ ...input,
22024
22174
  publicationId: publicationId ?? "00000000-0000-4000-8000-000000000000"
22025
22175
  });
22026
- return captured;
22176
+ return input;
22027
22177
  };
22028
22178
  return {
22179
+ create(record2) {
22180
+ if (publicationId)
22181
+ throw new DataOperationError("invalid_change", false, "unapplied");
22182
+ const next = [...creates, captureDataCreate(record2)];
22183
+ captureDataChanges(updates, next, deletes);
22184
+ checkDataRequestSize({
22185
+ repository,
22186
+ operation: "changes.publish",
22187
+ updates,
22188
+ creates: next,
22189
+ ...deletes.length ? { deletes } : {},
22190
+ publicationId: "00000000-0000-4000-8000-000000000000"
22191
+ });
22192
+ creates = next;
22193
+ },
22029
22194
  update(record2, set) {
22030
22195
  if (publicationId)
22031
22196
  throw new DataOperationError("invalid_change", false, "unapplied");
22032
22197
  const next = [...updates, captureDataUpdate(record2, set)];
22033
- captureDataUpdates(next);
22198
+ captureDataChanges(next, creates, deletes);
22034
22199
  checkDataRequestSize({
22035
22200
  repository,
22036
22201
  operation: "changes.publish",
22037
22202
  updates: next,
22203
+ ...creates.length ? { creates } : {},
22204
+ ...deletes.length ? { deletes } : {},
22038
22205
  publicationId: "00000000-0000-4000-8000-000000000000"
22039
22206
  });
22040
22207
  updates = next;
22041
22208
  },
22209
+ delete(record2) {
22210
+ if (publicationId)
22211
+ throw new DataOperationError("invalid_change", false, "unapplied");
22212
+ const next = [...deletes, captureDataDelete(record2)];
22213
+ captureDataChanges(updates, creates, next);
22214
+ checkDataRequestSize({
22215
+ repository,
22216
+ operation: "changes.publish",
22217
+ updates,
22218
+ deletes: next,
22219
+ ...creates.length ? { creates } : {},
22220
+ publicationId: "00000000-0000-4000-8000-000000000000"
22221
+ });
22222
+ deletes = next;
22223
+ },
22042
22224
  async validate() {
22043
- await send({ operation: "changes.validate", updates: snapshot() });
22225
+ await send({ operation: "changes.validate", ...snapshot() });
22044
22226
  },
22045
22227
  publish() {
22046
22228
  if (confirmed)
@@ -22053,12 +22235,12 @@ var data = {
22053
22235
  return Promise.reject(error);
22054
22236
  }
22055
22237
  publicationId ??= crypto.randomUUID();
22056
- pending = send({ operation: "changes.publish", updates: captured, publicationId }).then((result) => {
22238
+ pending = send({ operation: "changes.publish", ...captured, publicationId }).then((result) => {
22057
22239
  confirmed = structuredClone(result);
22058
22240
  return result;
22059
22241
  }).catch((error) => {
22060
22242
  if (error instanceof DataOperationError)
22061
- throw new DataOperationError(error.code, error.retryable, error.effect ?? (["unavailable", "publication_pending"].includes(error.code) ? "uncertain" : "unapplied"));
22243
+ throw new DataOperationError(error.code, error.retryable, error.effect ?? (["unavailable", "publication_pending"].includes(error.code) ? "uncertain" : "unapplied"), error.component);
22062
22244
  throw new DataOperationError("unavailable", true, "uncertain");
22063
22245
  }).finally(() => {
22064
22246
  pending = undefined;
@@ -22086,7 +22268,7 @@ var failures = {
22086
22268
  401: ["unauthenticated"],
22087
22269
  403: ["access_denied"],
22088
22270
  405: ["method_not_allowed"],
22089
- 409: ["conflict"],
22271
+ 409: ["conflict", "local_only"],
22090
22272
  413: ["request_too_large"],
22091
22273
  429: ["capacity"],
22092
22274
  503: ["integrity", "unavailable"]
@@ -22144,7 +22326,7 @@ async function result(response) {
22144
22326
  }
22145
22327
  }
22146
22328
  async function send(suffix, init, descriptor, initial, parent) {
22147
- const original = initial ?? await resolveFilesContext();
22329
+ const original = initial;
22148
22330
  const checkCurrent = () => {
22149
22331
  if (parent?.aborted)
22150
22332
  throw unavailable();
@@ -22153,37 +22335,71 @@ async function send(suffix, init, descriptor, initial, parent) {
22153
22335
  };
22154
22336
  for (let attempt = 0;attempt < ATTEMPTS; attempt++) {
22155
22337
  checkCurrent();
22156
- const context = attempt === 0 && !initial ? original : await resolveFilesContext();
22338
+ const context = await resolveFilesContext();
22157
22339
  checkCurrent();
22158
22340
  if (context.url !== original.url)
22159
22341
  throw new FileOperationError("unsupported_context", 400);
22160
22342
  try {
22161
- return await timed(async (signal) => {
22162
- const response = await fetch(`${context.url}${suffix}`, {
22343
+ return await (async () => {
22344
+ const response = context.local ? await (async () => {
22345
+ const body = init.body instanceof ArrayBuffer ? new Uint8Array(init.body) : undefined;
22346
+ let binary = "";
22347
+ if (body)
22348
+ for (let offset = 0;offset < body.length; offset += 8192)
22349
+ binary += String.fromCharCode(...body.subarray(offset, offset + 8192));
22350
+ const value2 = await context.local("files/request", {
22351
+ suffix,
22352
+ method: init.method,
22353
+ headers: Object.fromEntries(new Headers(init.headers)),
22354
+ ...body ? { bytes: btoa(binary) } : {}
22355
+ });
22356
+ if (value2.bytes !== undefined) {
22357
+ if (value2.bytes.length > Math.ceil(MAX_BYTES / 3) * 4)
22358
+ throw unavailable();
22359
+ const data2 = atob(value2.bytes);
22360
+ return new Response(Uint8Array.from(data2, (c) => c.charCodeAt(0)), { status: value2.status, headers: value2.headers });
22361
+ }
22362
+ return Response.json(value2.json, { status: value2.status, headers: value2.headers });
22363
+ })() : await fetch(`${context.url}${suffix}`, {
22163
22364
  ...init,
22164
- signal,
22365
+ signal: parent,
22165
22366
  headers: { ...context.headers, ...init.headers },
22166
22367
  redirect: "error",
22167
22368
  cache: "no-store"
22168
22369
  });
22169
22370
  if (!response.ok) {
22170
- const value = await result(response);
22171
- const error = value && typeof value === "object" && "error" in value ? value.error : undefined;
22371
+ const value2 = await result(response);
22372
+ const error = value2 && typeof value2 === "object" && "error" in value2 ? value2.error : undefined;
22172
22373
  const received = error && typeof error === "object" && "code" in error ? error.code : undefined;
22173
22374
  const code = failures[response.status]?.find((code2) => code2 === received);
22174
22375
  throw code ? new FileOperationError(code, response.status, code === "capacity" || code === "unavailable") : unavailable();
22175
22376
  }
22176
- if (response.status !== 200)
22377
+ if (response.status !== 200) {
22378
+ response.body?.cancel().catch(() => {});
22177
22379
  throw unavailable();
22178
- checkCurrent();
22380
+ }
22381
+ try {
22382
+ checkCurrent();
22383
+ } catch (error) {
22384
+ response.body?.cancel().catch(() => {});
22385
+ throw error;
22386
+ }
22179
22387
  if (!descriptor)
22180
22388
  return response;
22181
- const file = parseFileDescriptor(await result(response));
22182
- if (file.size > MAX_BYTES)
22183
- throw unavailable();
22389
+ const value = await result(response);
22390
+ let parsed;
22391
+ if (descriptor === "enrichment") {
22392
+ if (!value || typeof value !== "object" || Array.isArray(value))
22393
+ throw unavailable();
22394
+ const { name, ...metadata } = value;
22395
+ if (!safeEnrichmentName(name))
22396
+ throw unavailable();
22397
+ parsed = { name, ...parseEnrichmentDescriptor(metadata) };
22398
+ } else
22399
+ parsed = parseFileDescriptor(value);
22184
22400
  checkCurrent();
22185
- return file;
22186
- }, parent);
22401
+ return parsed;
22402
+ })();
22187
22403
  } catch (error) {
22188
22404
  const failure = error instanceof FileOperationError ? error : unavailable();
22189
22405
  if (!failure.retryable || attempt + 1 === ATTEMPTS)
@@ -22197,7 +22413,77 @@ function idPath(id2) {
22197
22413
  throw invalid();
22198
22414
  return `/${id2}`;
22199
22415
  }
22416
+ async function operation(work, initial, parent) {
22417
+ const context = initial ?? await resolveFilesContext();
22418
+ if (active >= MAX_ACTIVE)
22419
+ throw new FileOperationError("capacity", 429, true);
22420
+ active++;
22421
+ return timed(async (signal) => {
22422
+ try {
22423
+ return await work(context, signal);
22424
+ } finally {
22425
+ active--;
22426
+ }
22427
+ }, parent).catch((error) => {
22428
+ throw error instanceof FileOperationError ? error : unavailable();
22429
+ });
22430
+ }
22431
+ function contentPath(id2, name) {
22432
+ const path = idPath(id2);
22433
+ if (name === undefined)
22434
+ return `${path}/content`;
22435
+ if (!safeEnrichmentName(name))
22436
+ throw invalid();
22437
+ return `${path}/enrichments/${encodeURIComponent(name)}/content`;
22438
+ }
22439
+ async function read(id2, names) {
22440
+ const context = await resolveFilesContext();
22441
+ idPath(id2);
22442
+ if (names === undefined || typeof names === "string") {
22443
+ const path = contentPath(id2, names);
22444
+ return operation((context2, signal) => send(path, { method: "GET" }, false, context2, signal), context);
22445
+ }
22446
+ if (!Array.isArray(names))
22447
+ throw invalid();
22448
+ const distinct = [];
22449
+ const seen = new Set;
22450
+ for (const name of names) {
22451
+ if (typeof name !== "string" || !safeEnrichmentName(name))
22452
+ throw invalid();
22453
+ if (seen.has(name))
22454
+ continue;
22455
+ if (seen.size === 16)
22456
+ throw invalid();
22457
+ seen.add(name);
22458
+ distinct.push(name);
22459
+ }
22460
+ const paths = distinct.map((name) => contentPath(id2, name));
22461
+ return timed(async (parent) => {
22462
+ const responses = {};
22463
+ try {
22464
+ for (let i = 0;i < paths.length; i++) {
22465
+ const response = await operation((context2, signal) => send(paths[i], { method: "GET" }, false, context2, signal), context, parent);
22466
+ Object.defineProperty(responses, distinct[i], { value: response, enumerable: true });
22467
+ }
22468
+ if (!context.isCurrent())
22469
+ throw new FileOperationError("unsupported_context", 400);
22470
+ return responses;
22471
+ } catch (error) {
22472
+ for (const response of Object.values(responses))
22473
+ response.body?.cancel().catch(() => {});
22474
+ throw error;
22475
+ }
22476
+ });
22477
+ }
22200
22478
  var files = {
22479
+ async upload(id2) {
22480
+ return operation(async (context, signal) => {
22481
+ const file = await send(`${idPath(id2)}/upload`, { method: "POST" }, true, context, signal);
22482
+ if (file.id !== id2)
22483
+ throw unavailable();
22484
+ return file;
22485
+ });
22486
+ },
22201
22487
  async create(input, options) {
22202
22488
  const context = await resolveFilesContext();
22203
22489
  if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((key) => !["file", "size", "path", "name", "storage", "csv"].includes(key)) || options !== undefined && (!options || typeof options !== "object" || Array.isArray(options) || Object.keys(options).some((key) => key !== "requestKey")))
@@ -22222,6 +22508,8 @@ var files = {
22222
22508
  csv: input.csv,
22223
22509
  requestKey: options?.requestKey === undefined ? crypto.randomUUID() : options.requestKey
22224
22510
  }, MAX_BYTES);
22511
+ if (!context.local && metadata.storage === "local")
22512
+ throw new FileOperationError("unsupported_context", 400);
22225
22513
  const header = encodeURIComponent(JSON.stringify(metadata));
22226
22514
  if (new TextEncoder().encode(header).byteLength > METADATA_BYTES)
22227
22515
  throw invalid();
@@ -22247,12 +22535,45 @@ var files = {
22247
22535
  throw error instanceof FileOperationError ? error : unavailable();
22248
22536
  });
22249
22537
  },
22538
+ async enrich(input) {
22539
+ const context = await resolveFilesContext();
22540
+ if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((key) => !["file", "name", "content", "mediaType", "replace"].includes(key)) || typeof input.content !== "string" && !(input.content instanceof Uint8Array))
22541
+ throw invalid();
22542
+ const path = `${idPath(input.file)}/enrichments`;
22543
+ if (typeof input.content === "string" ? input.content.length > MAX_BYTES : input.content.byteLength > MAX_BYTES)
22544
+ throw new FileOperationError("request_too_large", 413);
22545
+ return operation(async (context2, signal) => {
22546
+ let bytes;
22547
+ if (typeof input.content === "string") {
22548
+ const buffer = new Uint8Array(Math.min(MAX_BYTES, input.content.length * 3));
22549
+ const encoded = new TextEncoder().encodeInto(input.content, buffer);
22550
+ if (encoded.read !== input.content.length)
22551
+ throw new FileOperationError("request_too_large", 413);
22552
+ bytes = buffer.slice(0, encoded.written);
22553
+ } else
22554
+ bytes = input.content.slice();
22555
+ const metadata = parseFileEnrichMetadata({
22556
+ name: input.name,
22557
+ size: bytes.byteLength,
22558
+ ...input.mediaType === undefined ? {} : { mediaType: input.mediaType },
22559
+ ...input.replace === undefined ? {} : { replace: input.replace }
22560
+ }, MAX_BYTES);
22561
+ const header = encodeURIComponent(JSON.stringify(metadata));
22562
+ if (new TextEncoder().encode(header).byteLength > METADATA_BYTES)
22563
+ throw invalid();
22564
+ const result2 = await send(path, { method: "POST", headers: {
22565
+ "content-type": "application/octet-stream",
22566
+ "x-oddington-enrichment": header
22567
+ }, body: bytes.buffer }, "enrichment", context2, signal);
22568
+ if (result2.name !== metadata.name)
22569
+ throw unavailable();
22570
+ return result2;
22571
+ }, context);
22572
+ },
22250
22573
  async get(id2) {
22251
- return send(idPath(id2), { method: "GET" }, true);
22574
+ return operation((context, signal) => send(idPath(id2), { method: "GET" }, true, context, signal));
22252
22575
  },
22253
- async read(id2) {
22254
- return send(`${idPath(id2)}/content`, { method: "GET" }, false);
22255
- }
22576
+ read
22256
22577
  };
22257
22578
  // src/os.ts
22258
22579
  var installedDispose;
@@ -22336,16 +22657,16 @@ function routinePublicationClient(options = {}) {
22336
22657
  } : async () => {
22337
22658
  throw new Error("Member publication requires a host-supplied request");
22338
22659
  });
22339
- async function send2(operation, body) {
22660
+ async function send2(operation2, body) {
22340
22661
  if (local)
22341
- return await local(operation, body);
22342
- const response = await request(`/api/cli/workspaces/${workspace}/inbox/${operation}`, {
22662
+ return await local(operation2, body);
22663
+ const response = await request(`/api/cli/workspaces/${workspace}/inbox/${operation2}`, {
22343
22664
  method: "POST",
22344
22665
  headers: { "content-type": "application/json", ...headers },
22345
22666
  body: JSON.stringify(body)
22346
22667
  });
22347
22668
  const result2 = await response.json();
22348
- if (!response.ok && operation === "records/update")
22669
+ if (!response.ok && operation2 === "records/update")
22349
22670
  throw new RoutineRecordWriteError(result2.error ?? "Could not confirm routine preparation", result2.effect ?? "uncertain");
22350
22671
  if (!response.ok)
22351
22672
  throw new Error(result2.error ?? "Could not confirm routine publication");
@@ -13,16 +13,22 @@ export { AirtableOperationError } from '../../../shared/airtable-contract.js';
13
13
  export type { AirtableField, AirtableTable, AirtableRecord, AirtableListInput, AirtableGetInput, AirtableCreateInput, AirtableUpdateInput, AirtableDeleteInput, AirtableReceipt, AirtableWriteResult, AirtableClient, Connections } from '../../../shared/airtable-contract.js';
14
14
  export declare const connections: import('../../../shared/airtable-contract.js').Connections;
15
15
  export { DataOperationError } from '../../../shared/data-contract.js';
16
- export type { DataRepository, DataErrorCode, PublicValueDefinition, PublicField, CollectionSummary, CollectionSchema, QueryScalar, SelectionOptions, RecordSelection, RecordsQuery, DataValue, SelectedRecord, SelectionMetadata, RecordsResult, RecordIdentity, RecordUpdateSet, DataPublication, DataChangeSet, DataEffect } from '../../../shared/data-contract.js';
16
+ export type { DataRepository, DataErrorCode, PublicValueDefinition, PublicField, CollectionSummary, CollectionSchema, QueryScalar, SelectionOptions, RecordSelection, RecordsQuery, DataValue, SelectedRecord, SelectionMetadata, RecordsResult, RecordIdentity, RecordCreation, RecordUpdateSet, DataPublication, DataChangeSet, DataEffect } from '../../../shared/data-contract.js';
17
17
  export declare const data: {
18
18
  repository(name: string): import('../../../shared/data-contract.js').DataRepository;
19
19
  };
20
20
  export { FileOperationError } from '../../../shared/files-contract.js';
21
- export type { File, FileInput, FileStorage, FileCreateInput, FileCreateOptions, FileErrorCode } from '../../../shared/files-contract.js';
21
+ export type { File, FileInput, FileStorage, FileCreateInput, FileCreateOptions, FileEnrichment, FileEnrichOptions, FileEnrichResult, FileErrorCode } from '../../../shared/files-contract.js';
22
22
  export declare const files: {
23
23
  create(input: import('../../../shared/files-contract.js').FileCreateInput, options?: import('../../../shared/files-contract.js').FileCreateOptions): Promise<import('../../../shared/files-contract.js').File>;
24
24
  get(id: string): Promise<import('../../../shared/files-contract.js').File>;
25
+ upload(id: string): Promise<import('../../../shared/files-contract.js').File>;
26
+ enrich(input: import('../../../shared/files-contract.js').FileEnrichOptions): Promise<import('../../../shared/files-contract.js').FileEnrichResult>;
25
27
  read(id: string): Promise<Response>;
28
+ read(id: string, name: string): Promise<Response>;
29
+ read<const Names extends readonly string[]>(id: string, names: Names): Promise<{
30
+ [Name in Names[number]]: Response;
31
+ }>;
26
32
  };
27
33
  /** The host's existing refreshing session source; supplies identity, never membership. */
28
34
  export type HostedSessionSource = {
@@ -108,14 +108,23 @@ export type RecordsResult = {
108
108
  };
109
109
  export type RecordIdentity = Pick<SelectedRecord, '$ref' | '$version'>;
110
110
  export type RecordUpdateSet = Record<string, DataValue>;
111
+ export type RecordCreation = {
112
+ $ref: string;
113
+ $filing?: Record<string, string>;
114
+ $version?: never;
115
+ $path?: never;
116
+ [field: string]: DataValue | undefined;
117
+ };
111
118
  export type DataPublication = {
112
119
  version: string;
113
- created: [];
120
+ created: RecordIdentity[];
114
121
  updated: RecordIdentity[];
115
- deleted: [];
122
+ deleted: RecordIdentity[];
116
123
  };
117
124
  export interface DataChangeSet {
125
+ create(record: RecordCreation): void;
118
126
  update(record: RecordIdentity, set: RecordUpdateSet): void;
127
+ delete(record: RecordIdentity): void;
119
128
  validate(): Promise<void>;
120
129
  publish(): Promise<DataPublication>;
121
130
  }
@@ -135,6 +144,7 @@ export declare class DataOperationError extends Error {
135
144
  readonly code: DataErrorCode;
136
145
  readonly retryable: boolean;
137
146
  readonly effect?: DataEffect | undefined;
138
- constructor(code: DataErrorCode, retryable?: boolean, effect?: DataEffect | undefined);
147
+ readonly component?: string | undefined;
148
+ constructor(code: DataErrorCode, retryable?: boolean, effect?: DataEffect | undefined, component?: string | undefined);
139
149
  }
140
150
  export {};
@@ -1,5 +1,5 @@
1
1
  /** Provider-neutral authored contract. Registries and object locations stay server-side. */
2
- export type FileStorage = 'local' | 'remote' | 'both';
2
+ export type FileStorage = 'local' | 'remote';
3
3
  export type FileInput = Blob | Uint8Array | ReadableStream<Uint8Array>;
4
4
  export type FileCsvHints = {
5
5
  encoding?: 'utf-8' | 'windows-1252';
@@ -17,6 +17,21 @@ export type FileCreateInput = {
17
17
  export type FileCreateOptions = {
18
18
  requestKey?: string;
19
19
  };
20
+ export type FileEnrichment = {
21
+ mediaType: string;
22
+ size: number;
23
+ sha256: string;
24
+ };
25
+ export type FileEnrichOptions = {
26
+ file: string;
27
+ name: string;
28
+ content: string | Uint8Array;
29
+ mediaType?: string;
30
+ replace?: string;
31
+ };
32
+ export type FileEnrichResult = FileEnrichment & {
33
+ name: string;
34
+ };
20
35
  export type File = {
21
36
  id: string;
22
37
  name: string;
@@ -24,11 +39,7 @@ export type File = {
24
39
  mediaType: string;
25
40
  size: number;
26
41
  sha256: string;
27
- enrichments: Record<string, {
28
- mediaType: string;
29
- size: number;
30
- sha256: string;
31
- }>;
42
+ enrichments: Record<string, FileEnrichment>;
32
43
  details: {
33
44
  kind: 'csv';
34
45
  encoding: 'utf-8' | 'windows-1252';
@@ -42,7 +53,7 @@ export type File = {
42
53
  kind: 'other';
43
54
  };
44
55
  };
45
- export type FileErrorCode = 'invalid_input' | 'unsupported_context' | 'unauthenticated' | 'access_denied' | 'request_too_large' | 'conflict' | 'integrity' | 'unavailable' | 'capacity' | 'method_not_allowed';
56
+ export type FileErrorCode = 'invalid_input' | 'unsupported_context' | 'unauthenticated' | 'access_denied' | 'request_too_large' | 'conflict' | 'local_only' | 'integrity' | 'unavailable' | 'capacity' | 'method_not_allowed';
46
57
  export declare class FileOperationError extends Error {
47
58
  readonly code: FileErrorCode;
48
59
  readonly status: number;
@@ -52,13 +63,22 @@ export declare class FileOperationError extends Error {
52
63
  export declare const FILE_ID_PATTERN: RegExp;
53
64
  /** Treat descriptor transport as untrusted; no provider fields escape the boundary. */
54
65
  export declare function parseFileDescriptor(value: unknown): File;
66
+ export declare function parseEnrichmentDescriptor(value: unknown): FileEnrichment;
67
+ export declare function safeEnrichmentName(value: unknown): value is string;
68
+ export type FileEnrichMetadata = {
69
+ name: string;
70
+ size: number;
71
+ mediaType?: string;
72
+ replace?: string;
73
+ };
74
+ export declare function parseFileEnrichMetadata(value: unknown, maxBytes: number): FileEnrichMetadata;
55
75
  export declare function safeFileName(value: unknown): value is string;
56
76
  export declare function safeFilePath(value: unknown): value is string;
57
77
  /** Private transport metadata: normalized material options exclude the request key and size declaration. */
58
78
  export type FileCreateMetadata = {
59
79
  name: string;
60
80
  path: string;
61
- storage: 'remote';
81
+ storage?: FileStorage;
62
82
  csv?: Required<FileCsvHints>;
63
83
  size: number;
64
84
  requestKey: string;
@@ -12,7 +12,11 @@ export type RecordDeleteRequest = {
12
12
  repository: string;
13
13
  records: RecordDelete[];
14
14
  };
15
- export type RecordChangeRequest = RecordWriteRequest | RecordDeleteRequest;
15
+ export type RecordChange = RecordWrite | RecordDelete;
16
+ export type RecordChangeRequest = {
17
+ repository: string;
18
+ records: RecordChange[];
19
+ };
16
20
  export type RoutineRecordWriteRequest = {
17
21
  input: RecordChangeRequest;
18
22
  requestKey: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oddington/sdk",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Oddington routine and response-handler SDK",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",