@oddington/sdk 0.1.3 → 0.1.4

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  The importable client for Oddington apps, routines and response handlers.
4
4
  Node operations require Node.js 22.12.0 or later. The root import and hosted
5
- Data API work in browsers. Installing the package grants no authority.
5
+ Data and Files APIs work in browsers. Installing the package grants no authority.
6
6
 
7
7
  ```sh
8
8
  npm install @oddington/sdk
@@ -29,6 +29,36 @@ session subscription. Workspace existence and member access are checked by the
29
29
  OS service. App modules use capability namespaces; routines and handlers receive
30
30
  runtime context without calling `os.init`.
31
31
 
32
+ Create and recover a private immutable original through the same host context:
33
+
34
+ ```ts
35
+ import { files } from '@oddington/sdk';
36
+
37
+ const report = await files.create({
38
+ file: selectedFile, // Web File, path-backed Blob, Uint8Array or byte stream
39
+ name: 'September statement.csv',
40
+ path: 'HSC/2026/09',
41
+ }, { requestKey: 'statement/2026/09' });
42
+ const descriptor = await files.get(report.id);
43
+ const response = await files.read(descriptor.id);
44
+ await response.text(); // Also supports blob(), arrayBuffer() and body streaming.
45
+ ```
46
+
47
+ Hosted Files retains remote originals only, with empty `enrichments`. It rejects
48
+ `local`, `both` and explicit routine local preparation with `unsupported_context`.
49
+ Unnamed inputs require a safe `name`; streams also require declared `size`.
50
+ 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;
53
+ transport retries at most once with the same prepared bytes and request key.
54
+ Omitting a key generates one for that invocation. Supply a stable key to recover
55
+ 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,
58
+ status and retryability without server details. Reads return the successful Web
59
+ Response without consuming its body. Creation grants private creator access;
60
+ workspace membership alone never grants another creator's File.
61
+
32
62
  Read a named repository through the authenticated host context:
33
63
 
34
64
  ```ts
@@ -62,13 +92,60 @@ Each graph selection sends one request and returns only selected canonical
62
92
  values, expanded forward links and exact opaque public versions. Cursors
63
93
  continue the exact retained artifact and collection selection, even after
64
94
  replacement; repeat its options and links while omitting unrelated collections.
65
- No automatic retry, separate version fetch, search, history or changes operation
66
- is included. Collection schemas omit Markdown storage hints.
95
+ No automatic retry, separate version fetch, search or history operation is
96
+ included. Collection schemas omit Markdown storage hints.
97
+
98
+ `where` combines field conditions with AND. Alongside exact scalar/null equality
99
+ and `$in`, declared one-valued `integer`, `number` and `date` fields accept
100
+ `$gt`, `$gte`, `$lt` and `$lte`:
101
+
102
+ ```ts
103
+ await general.records.select({ invoices: { $: { where: {
104
+ amount: { $gte: 10, $lte: 20 },
105
+ date: { $gte: '2026-10-01', $lt: '2026-11-01' },
106
+ month: { $gte: '2026-10', $lte: '2026-10' },
107
+ }, count: true } } });
108
+ ```
109
+
110
+ Supply one lower bound, one upper bound, or one of each. Bounds cannot combine
111
+ `$gt` with `$gte`, `$lt` with `$lte`, or any bound with `$in`. Operands must be
112
+ valid for the declared field, including numeric precision and real day/month
113
+ dates; values are never coerced. Missing and null values never match bounds.
114
+ A valid interval without matches returns an empty array and requested zero
115
+ count. V0 types check expression structure; runtime schema validation owns field
116
+ eligibility. Repeat the same bounds when continuing a cursor.
117
+
67
118
  `DataOperationError.code` distinguishes `context_unavailable`, `unauthenticated`,
68
119
  `access_denied`, `invalid_query`, `request_too_large`, `capacity` and `unavailable`.
69
- Only `capacity` and `unavailable` are retryable; an error never includes private
120
+ For reads, only `capacity` and `unavailable` are retryable; an error never includes private
70
121
  server details. Server boundaries own query bounds and authorization.
71
122
 
123
+ Guarded updates use the same handle in hosted and explicitly local routines:
124
+
125
+ ```ts
126
+ const changes = general.changes();
127
+ changes.update(first.invoices[0], { amount: 30 });
128
+ await changes.validate();
129
+ const publication = await changes.publish();
130
+ ```
131
+
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: [] }`
136
+ 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
138
+ advances only run-local reads. `os routine publish` later applies and indexes
139
+ that immutable proposal; unrelated staged and unstaged work is preserved.
140
+
141
+ `invalid_change` and known-unapplied `conflict` reject the batch.
142
+ `publication_pending` is retryable and may report `uncertain` or `applied`;
143
+ retry the same change set to reconcile its retained identity. A transport failure
144
+ also keeps that identity. Concurrent publication calls coalesce, confirmed calls
145
+ 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.
148
+
72
149
  Bind a declared Airtable connection with `connections.airtable('project-management')`
73
150
  from the package root. Import and binding perform no network or credential work.
74
151
  The client exposes `tables.list()` and `records.list`, `get`, `create`, `update`
package/dist/index.js CHANGED
@@ -691,8 +691,8 @@ function __awaiter(thisArg, _arguments, P, generator) {
691
691
  reject(e);
692
692
  }
693
693
  }
694
- function step(result) {
695
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
694
+ function step(result2) {
695
+ result2.done ? resolve(result2.value) : adopt(result2.value).then(fulfilled, rejected);
696
696
  }
697
697
  step((generator = generator.apply(thisArg, _arguments || [])).next());
698
698
  });
@@ -1332,19 +1332,19 @@ var require_common = __commonJS((exports) => {
1332
1332
  }
1333
1333
  },
1334
1334
  flattenChunks: function(chunks) {
1335
- var i2, l, len, pos, chunk, result;
1335
+ var i2, l, len, pos, chunk, result2;
1336
1336
  len = 0;
1337
1337
  for (i2 = 0, l = chunks.length;i2 < l; i2++) {
1338
1338
  len += chunks[i2].length;
1339
1339
  }
1340
- result = new Uint8Array(len);
1340
+ result2 = new Uint8Array(len);
1341
1341
  pos = 0;
1342
1342
  for (i2 = 0, l = chunks.length;i2 < l; i2++) {
1343
1343
  chunk = chunks[i2];
1344
- result.set(chunk, pos);
1344
+ result2.set(chunk, pos);
1345
1345
  pos += chunk.length;
1346
1346
  }
1347
- return result;
1347
+ return result2;
1348
1348
  }
1349
1349
  };
1350
1350
  var fnUntyped = {
@@ -3130,11 +3130,11 @@ var require_strings = __commonJS((exports) => {
3130
3130
  return String.fromCharCode.apply(null, utils.shrinkBuf(buf, len));
3131
3131
  }
3132
3132
  }
3133
- var result = "";
3133
+ var result2 = "";
3134
3134
  for (var i2 = 0;i2 < len; i2++) {
3135
- result += String.fromCharCode(buf[i2]);
3135
+ result2 += String.fromCharCode(buf[i2]);
3136
3136
  }
3137
- return result;
3137
+ return result2;
3138
3138
  }
3139
3139
  exports.buf2binstring = function(buf) {
3140
3140
  return buf2binstring(buf, buf.length);
@@ -7323,40 +7323,40 @@ var init_PDFContext = __esm(() => {
7323
7323
  types[_i - 1] = arguments[_i];
7324
7324
  }
7325
7325
  var preservePDFNull = types.includes(PDFNull_default);
7326
- var result = ref instanceof PDFRef_default ? this.indirectObjects.get(ref) : ref;
7327
- if (!result || result === PDFNull_default && !preservePDFNull)
7326
+ var result2 = ref instanceof PDFRef_default ? this.indirectObjects.get(ref) : ref;
7327
+ if (!result2 || result2 === PDFNull_default && !preservePDFNull)
7328
7328
  return;
7329
7329
  for (var idx3 = 0, len2 = types.length;idx3 < len2; idx3++) {
7330
7330
  var type = types[idx3];
7331
7331
  if (type === PDFNull_default) {
7332
- if (result === PDFNull_default)
7333
- return result;
7332
+ if (result2 === PDFNull_default)
7333
+ return result2;
7334
7334
  } else {
7335
- if (result instanceof type)
7336
- return result;
7335
+ if (result2 instanceof type)
7336
+ return result2;
7337
7337
  }
7338
7338
  }
7339
- throw new UnexpectedObjectTypeError(types, result);
7339
+ throw new UnexpectedObjectTypeError(types, result2);
7340
7340
  };
7341
7341
  PDFContext2.prototype.lookup = function(ref) {
7342
7342
  var types = [];
7343
7343
  for (var _i = 1;_i < arguments.length; _i++) {
7344
7344
  types[_i - 1] = arguments[_i];
7345
7345
  }
7346
- var result = ref instanceof PDFRef_default ? this.indirectObjects.get(ref) : ref;
7346
+ var result2 = ref instanceof PDFRef_default ? this.indirectObjects.get(ref) : ref;
7347
7347
  if (types.length === 0)
7348
- return result;
7348
+ return result2;
7349
7349
  for (var idx3 = 0, len2 = types.length;idx3 < len2; idx3++) {
7350
7350
  var type = types[idx3];
7351
7351
  if (type === PDFNull_default) {
7352
- if (result === PDFNull_default)
7353
- return result;
7352
+ if (result2 === PDFNull_default)
7353
+ return result2;
7354
7354
  } else {
7355
- if (result instanceof type)
7356
- return result;
7355
+ if (result2 instanceof type)
7356
+ return result2;
7357
7357
  }
7358
7358
  }
7359
- throw new UnexpectedObjectTypeError(types, result);
7359
+ throw new UnexpectedObjectTypeError(types, result2);
7360
7360
  };
7361
7361
  PDFContext2.prototype.getObjectRef = function(pdfObject) {
7362
7362
  var entries = Array.from(this.indirectObjects.entries());
@@ -15922,13 +15922,13 @@ var cx = 0, cy = 0, px = 0, py = 0, sx = 0, sy = 0, parameters, parse = function
15922
15922
  thArc -= 2 * Math.PI;
15923
15923
  }
15924
15924
  var segments = Math.ceil(Math.abs(thArc / (Math.PI * 0.5 + 0.001)));
15925
- var result = [];
15925
+ var result2 = [];
15926
15926
  for (var i3 = 0;i3 < segments; i3++) {
15927
15927
  var th2 = th0 + i3 * thArc / segments;
15928
15928
  var th3 = th0 + (i3 + 1) * thArc / segments;
15929
- result[i3] = [xc, yc, th2, th3, rx, ry, sinTh, cosTh];
15929
+ result2[i3] = [xc, yc, th2, th3, rx, ry, sinTh, cosTh];
15930
15930
  }
15931
- return result;
15931
+ return result2;
15932
15932
  }, segmentToBezier = function(cx1, cy1, th0, th1, rx, ry, sinTh, cosTh) {
15933
15933
  var a00 = cosTh * rx;
15934
15934
  var a01 = -sinTh * ry;
@@ -15942,7 +15942,7 @@ var cx = 0, cy = 0, px = 0, py = 0, sx = 0, sy = 0, parameters, parse = function
15942
15942
  var y3 = cy1 + Math.sin(th1);
15943
15943
  var x2 = x3 + t * Math.sin(th1);
15944
15944
  var y2 = y3 - t * Math.cos(th1);
15945
- var result = [
15945
+ var result2 = [
15946
15946
  a00 * x1 + a01 * y1,
15947
15947
  a10 * x1 + a11 * y1,
15948
15948
  a00 * x2 + a01 * y2,
@@ -15950,7 +15950,7 @@ var cx = 0, cy = 0, px = 0, py = 0, sx = 0, sy = 0, parameters, parse = function
15950
15950
  a00 * x3 + a01 * y3,
15951
15951
  a10 * x3 + a11 * y3
15952
15952
  ];
15953
- return result;
15953
+ return result2;
15954
15954
  }, svgPathToOperators = function(path) {
15955
15955
  return apply(parse(path));
15956
15956
  };
@@ -21684,14 +21684,167 @@ var connections = createConnections(async (name, operation, input) => {
21684
21684
  class DataOperationError extends Error {
21685
21685
  code;
21686
21686
  retryable;
21687
- constructor(code, retryable = false) {
21687
+ effect;
21688
+ constructor(code, retryable = false, effect) {
21688
21689
  super(`Data operation failed: ${code}`);
21689
21690
  this.code = code;
21690
21691
  this.retryable = retryable;
21692
+ this.effect = effect;
21691
21693
  this.name = "DataOperationError";
21692
21694
  }
21693
21695
  }
21694
21696
 
21697
+ // ../../shared/data-envelope.ts
21698
+ var DATA_REQUEST_LIMIT = 256 * 1024;
21699
+ var invalidChange = () => {
21700
+ throw new DataOperationError("invalid_change", false, "unapplied");
21701
+ };
21702
+ function mapping(value) {
21703
+ if (!value || typeof value !== "object" || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
21704
+ return invalidChange();
21705
+ if (Reflect.ownKeys(value).some((key) => typeof key !== "string" || !Object.getOwnPropertyDescriptor(value, key)?.enumerable || !Object.hasOwn(Object.getOwnPropertyDescriptor(value, key), "value")))
21706
+ return invalidChange();
21707
+ return value;
21708
+ }
21709
+ function captureValue(value, ancestors = new Set, depth = 0) {
21710
+ if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value))
21711
+ return value;
21712
+ if (!value || typeof value !== "object" || ancestors.has(value) || depth > 32)
21713
+ return invalidChange();
21714
+ ancestors.add(value);
21715
+ try {
21716
+ if (Array.isArray(value)) {
21717
+ if (value.length > 1e5 || Object.keys(value).length !== value.length || Object.getOwnPropertySymbols(value).length)
21718
+ return invalidChange();
21719
+ return Array.from(value, (item) => captureValue(item, ancestors, depth + 1));
21720
+ }
21721
+ return Object.fromEntries(Object.entries(mapping(value)).map(([key, item]) => {
21722
+ if (["__proto__", "constructor", "prototype"].includes(key))
21723
+ return invalidChange();
21724
+ return [key, captureValue(item, ancestors, depth + 1)];
21725
+ }));
21726
+ } finally {
21727
+ ancestors.delete(value);
21728
+ }
21729
+ }
21730
+ 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)))
21733
+ return invalidChange();
21734
+ return { ref: identity.$ref, expectedVersion: identity.$version, set: captureValue(fields3) };
21735
+ }
21736
+ function captureDataUpdates(value) {
21737
+ if (!Array.isArray(value) || !value.length || value.length > 1e4)
21738
+ return invalidChange();
21739
+ const updates = value.map((item) => {
21740
+ const row = mapping(item);
21741
+ if (Object.keys(row).sort().join(",") !== "expectedVersion,ref,set")
21742
+ return invalidChange();
21743
+ return captureDataUpdate({ $ref: row.ref, $version: row.expectedVersion }, row.set);
21744
+ });
21745
+ if (new Set(updates.map((update) => update.ref)).size !== updates.length)
21746
+ return invalidChange();
21747
+ return updates;
21748
+ }
21749
+ function checkDataRequestSize(input) {
21750
+ if (new TextEncoder().encode(JSON.stringify(input)).byteLength > DATA_REQUEST_LIMIT)
21751
+ throw new DataOperationError("request_too_large", false, "unapplied");
21752
+ }
21753
+ function dataTransportError(status, value) {
21754
+ const failures = {
21755
+ 400: ["invalid_query", "invalid_change"],
21756
+ 401: ["unauthenticated"],
21757
+ 403: ["access_denied"],
21758
+ 409: ["conflict", "publication_pending"],
21759
+ 413: ["request_too_large"],
21760
+ 429: ["capacity"],
21761
+ 503: ["unavailable", "publication_pending"]
21762
+ };
21763
+ const error = value && typeof value === "object" && "error" in value ? value.error : undefined;
21764
+ const row = error && typeof error === "object" ? error : undefined;
21765
+ const code = failures[status]?.find((allowed) => allowed === row?.code) ?? "unavailable";
21766
+ 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);
21768
+ }
21769
+
21770
+ // ../../shared/files-contract.ts
21771
+ class FileOperationError extends Error {
21772
+ code;
21773
+ status;
21774
+ retryable;
21775
+ constructor(code, status, retryable = false) {
21776
+ super(code);
21777
+ this.code = code;
21778
+ this.status = status;
21779
+ this.retryable = retryable;
21780
+ }
21781
+ }
21782
+ var FILE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
21783
+ function parseFileDescriptor(value) {
21784
+ const invalid = () => {
21785
+ throw new FileOperationError("unavailable", 503, true);
21786
+ };
21787
+ if (!value || typeof value !== "object" || Array.isArray(value))
21788
+ return invalid();
21789
+ 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))
21791
+ return invalid();
21792
+ const d = f.details;
21793
+ if (d.kind === "other") {
21794
+ if (Object.keys(d).length !== 1 || f.mediaType !== "application/octet-stream")
21795
+ return invalid();
21796
+ } else if (d.kind === "csv") {
21797
+ if (Object.keys(d).sort().join(",") !== "dataRows,delimiter,encoding,headerRow,kind" || f.mediaType !== "text/csv" || !["utf-8", "windows-1252"].includes(d.encoding) || ![",", ";", "\t"].includes(d.delimiter) || !Number.isSafeInteger(d.headerRow) || d.headerRow < 1 || !Number.isSafeInteger(d.dataRows) || d.dataRows < 0)
21798
+ return invalid();
21799
+ } else
21800
+ return invalid();
21801
+ return f;
21802
+ }
21803
+ function safeFileName(value) {
21804
+ 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
+ }
21806
+ function safeFilePath(value) {
21807
+ return typeof value === "string" && value.length <= 1024 && value.split("/").every(safeFileName);
21808
+ }
21809
+ function parseFileCreateMetadata(value, maxBytes) {
21810
+ const invalid = () => {
21811
+ throw new FileOperationError("invalid_input", 400);
21812
+ };
21813
+ if (!value || typeof value !== "object" || Array.isArray(value))
21814
+ return invalid();
21815
+ const input = value;
21816
+ if (Object.keys(input).some((key) => !["name", "path", "storage", "csv", "size", "requestKey"].includes(key)))
21817
+ return invalid();
21818
+ const path = input.path === undefined ? "Files" : input.path;
21819
+ if (!safeFileName(input.name) || !safeFilePath(path))
21820
+ 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")
21824
+ return invalid();
21825
+ if (!Number.isSafeInteger(input.size) || input.size < 0)
21826
+ return invalid();
21827
+ if (input.size > maxBytes)
21828
+ throw new FileOperationError("request_too_large", 413);
21829
+ if (typeof input.requestKey !== "string" || input.requestKey.length < 1 || input.requestKey.length > 256 || input.requestKey !== input.requestKey.trim() || /[\u0000-\u001f\u007f]/.test(input.requestKey))
21830
+ return invalid();
21831
+ let csv;
21832
+ if (input.csv !== undefined) {
21833
+ if (!input.csv || typeof input.csv !== "object" || Array.isArray(input.csv))
21834
+ return invalid();
21835
+ const hints = input.csv;
21836
+ if (Object.keys(hints).some((key) => !["encoding", "delimiter", "headerRow"].includes(key)))
21837
+ return invalid();
21838
+ const encoding = hints.encoding === undefined ? "utf-8" : hints.encoding;
21839
+ const delimiter = hints.delimiter === undefined ? "," : hints.delimiter;
21840
+ const headerRow = hints.headerRow === undefined ? 1 : hints.headerRow;
21841
+ if (encoding !== "utf-8" && encoding !== "windows-1252" || ![",", ";", "\t"].includes(delimiter) || !Number.isSafeInteger(headerRow) || headerRow < 1 || headerRow > 1e5)
21842
+ return invalid();
21843
+ csv = { encoding, delimiter, headerRow };
21844
+ }
21845
+ return { name: input.name, path, storage: "remote", ...csv ? { csv } : {}, size: input.size, requestKey: input.requestKey };
21846
+ }
21847
+
21695
21848
  // ../../shared/os-origin.ts
21696
21849
  var DEFAULT_ORIGIN = "https://auth.oddington.ai";
21697
21850
  function oddingtonOrigin(environment = process.env) {
@@ -21703,14 +21856,6 @@ function oddingtonOrigin(environment = process.env) {
21703
21856
  }
21704
21857
 
21705
21858
  // src/routine-local-transport.ts
21706
- var dataFailures = {
21707
- 400: ["invalid_query"],
21708
- 401: ["unauthenticated"],
21709
- 403: ["access_denied"],
21710
- 413: ["request_too_large"],
21711
- 429: ["capacity"],
21712
- 503: ["unavailable"]
21713
- };
21714
21859
  function localRoutineRequest(environment = typeof process === "undefined" ? {} : process.env) {
21715
21860
  const {
21716
21861
  ODDINGTON_ROUTINE_LOCAL: mode,
@@ -21751,9 +21896,7 @@ function localRoutineRequest(environment = typeof process === "undefined" ? {} :
21751
21896
  const result = JSON.parse(Buffer.concat(chunks).toString());
21752
21897
  if (response.statusCode !== 200) {
21753
21898
  if (operation === "data/query") {
21754
- const received = result.error && typeof result.error === "object" && "code" in result.error ? result.error.code : undefined;
21755
- const code = dataFailures[response.statusCode ?? 0]?.find((allowed) => allowed === received) ?? "unavailable";
21756
- throw new DataOperationError(code, code === "capacity" || code === "unavailable");
21899
+ throw dataTransportError(response.statusCode ?? 0, result);
21757
21900
  }
21758
21901
  throw new Error(typeof result.error === "string" ? result.error : "Local preparation failed");
21759
21902
  }
@@ -21799,6 +21942,34 @@ async function resolveDataContext() {
21799
21942
  throw new DataOperationError("context_unavailable");
21800
21943
  }
21801
21944
  }
21945
+ async function resolveFilesContext() {
21946
+ try {
21947
+ 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;
21950
+ const host = memberContext;
21951
+ if (host) {
21952
+ const context = await host();
21953
+ if (!Number.isFinite(context.expiresAt) || context.expiresAt <= Date.now() || typeof context.accessToken !== "string" || !/^[^\s]{1,8192}$/.test(context.accessToken))
21954
+ throw new Error;
21955
+ return {
21956
+ ...filesContext(context.workspace, context.endpoint, { authorization: `Bearer ${context.accessToken}` }),
21957
+ isCurrent: context.isCurrent
21958
+ };
21959
+ }
21960
+ if (!/^[0-9a-f]{64}$/.test(environment.ODDINGTON_ROUTINE_RUN ?? ""))
21961
+ throw new Error;
21962
+ const workspace = environment.ODDINGTON_ROUTINE_WORKSPACE;
21963
+ return filesContext(workspace, `https://${workspace}.oddington.ai`, { "x-oddington-run": environment.ODDINGTON_ROUTINE_RUN });
21964
+ } catch {
21965
+ throw new FileOperationError("unsupported_context", 400);
21966
+ }
21967
+ }
21968
+ function filesContext(workspace, endpoint, headers) {
21969
+ if (!workspace || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(workspace) || workspace.length > 63 || ["auth", "www", "api", "os"].includes(workspace) || endpoint !== `https://${workspace}.oddington.ai`)
21970
+ throw new Error;
21971
+ return { url: `${endpoint}/api/files`, headers: { ...headers, "x-oddington-workspace": workspace }, isCurrent: () => true };
21972
+ }
21802
21973
  function checked(workspace, endpoint, headers) {
21803
21974
  if (!workspace || !/^[a-z0-9][a-z0-9-]{0,127}$/.test(workspace))
21804
21975
  throw new Error;
@@ -21809,19 +21980,12 @@ function checked(workspace, endpoint, headers) {
21809
21980
  }
21810
21981
 
21811
21982
  // src/data.ts
21812
- var failures = {
21813
- 400: ["invalid_query"],
21814
- 401: ["unauthenticated"],
21815
- 403: ["access_denied"],
21816
- 413: ["request_too_large"],
21817
- 429: ["capacity"],
21818
- 503: ["unavailable"]
21819
- };
21820
21983
  var data = {
21821
21984
  repository(repository) {
21822
21985
  async function send(input) {
21823
21986
  const context = await resolveDataContext();
21824
21987
  try {
21988
+ checkDataRequestSize({ repository, ...input });
21825
21989
  if ("local" in context)
21826
21990
  return await context.local("data/query", { repository, ...input });
21827
21991
  const response = await fetch(context.url, {
@@ -21833,12 +21997,7 @@ var data = {
21833
21997
  });
21834
21998
  const result = await response.json();
21835
21999
  if (!response.ok) {
21836
- const error = result && typeof result === "object" && "error" in result ? result.error : undefined;
21837
- const received = error && typeof error === "object" && "code" in error ? error.code : undefined;
21838
- const code = failures[response.status]?.find((allowed) => allowed === received);
21839
- if (code)
21840
- throw new DataOperationError(code, code === "capacity" || code === "unavailable");
21841
- throw new DataOperationError("unavailable", true);
22000
+ throw dataTransportError(response.status, result);
21842
22001
  }
21843
22002
  return result;
21844
22003
  } catch (error) {
@@ -21852,10 +22011,249 @@ var data = {
21852
22011
  list: () => send({ operation: "collections.list" }),
21853
22012
  schema: (name) => send({ operation: "collections.schema", name })
21854
22013
  },
21855
- records: { select: (query) => send({ operation: "records.select", query }) }
22014
+ records: { select: (query) => send({ operation: "records.select", query }) },
22015
+ changes() {
22016
+ let updates = [], publicationId;
22017
+ let pending, confirmed;
22018
+ const snapshot = () => {
22019
+ const captured = captureDataUpdates(updates);
22020
+ checkDataRequestSize({
22021
+ repository,
22022
+ operation: "changes.publish",
22023
+ updates: captured,
22024
+ publicationId: publicationId ?? "00000000-0000-4000-8000-000000000000"
22025
+ });
22026
+ return captured;
22027
+ };
22028
+ return {
22029
+ update(record2, set) {
22030
+ if (publicationId)
22031
+ throw new DataOperationError("invalid_change", false, "unapplied");
22032
+ const next = [...updates, captureDataUpdate(record2, set)];
22033
+ captureDataUpdates(next);
22034
+ checkDataRequestSize({
22035
+ repository,
22036
+ operation: "changes.publish",
22037
+ updates: next,
22038
+ publicationId: "00000000-0000-4000-8000-000000000000"
22039
+ });
22040
+ updates = next;
22041
+ },
22042
+ async validate() {
22043
+ await send({ operation: "changes.validate", updates: snapshot() });
22044
+ },
22045
+ publish() {
22046
+ if (confirmed)
22047
+ return Promise.resolve(structuredClone(confirmed));
22048
+ if (!pending) {
22049
+ let captured;
22050
+ try {
22051
+ captured = snapshot();
22052
+ } catch (error) {
22053
+ return Promise.reject(error);
22054
+ }
22055
+ publicationId ??= crypto.randomUUID();
22056
+ pending = send({ operation: "changes.publish", updates: captured, publicationId }).then((result) => {
22057
+ confirmed = structuredClone(result);
22058
+ return result;
22059
+ }).catch((error) => {
22060
+ if (error instanceof DataOperationError)
22061
+ throw new DataOperationError(error.code, error.retryable, error.effect ?? (["unavailable", "publication_pending"].includes(error.code) ? "uncertain" : "unapplied"));
22062
+ throw new DataOperationError("unavailable", true, "uncertain");
22063
+ }).finally(() => {
22064
+ pending = undefined;
22065
+ });
22066
+ }
22067
+ return pending.then((result) => structuredClone(result));
22068
+ }
22069
+ };
22070
+ }
21856
22071
  };
21857
22072
  }
21858
22073
  };
22074
+ // src/files.ts
22075
+ var MAX_BYTES = 16 * 1024 * 1024;
22076
+ var METADATA_BYTES = 4096;
22077
+ var RESULT_BYTES = 16 * 1024;
22078
+ var TIMEOUT_MS = 30000;
22079
+ var MAX_ACTIVE = 4;
22080
+ var ATTEMPTS = 2;
22081
+ var active = 0;
22082
+ var invalid = () => new FileOperationError("invalid_input", 400);
22083
+ var unavailable = () => new FileOperationError("unavailable", 503, true);
22084
+ var failures = {
22085
+ 400: ["invalid_input", "unsupported_context"],
22086
+ 401: ["unauthenticated"],
22087
+ 403: ["access_denied"],
22088
+ 405: ["method_not_allowed"],
22089
+ 409: ["conflict"],
22090
+ 413: ["request_too_large"],
22091
+ 429: ["capacity"],
22092
+ 503: ["integrity", "unavailable"]
22093
+ };
22094
+ async function boundedBytes(stream, limit) {
22095
+ if (!stream)
22096
+ return new Uint8Array;
22097
+ const reader = stream.getReader();
22098
+ const bytes = new Uint8Array(limit);
22099
+ let size = 0;
22100
+ try {
22101
+ for (;; ) {
22102
+ const { done, value } = await reader.read();
22103
+ if (done)
22104
+ break;
22105
+ if (!(value instanceof Uint8Array))
22106
+ throw invalid();
22107
+ size += value.byteLength;
22108
+ if (size > limit)
22109
+ throw new FileOperationError("request_too_large", 413);
22110
+ bytes.set(value, size - value.byteLength);
22111
+ }
22112
+ } finally {
22113
+ reader.releaseLock();
22114
+ }
22115
+ return bytes.slice(0, size);
22116
+ }
22117
+ async function timed(work, parent) {
22118
+ const controller = new AbortController;
22119
+ let timer;
22120
+ let abort;
22121
+ const expiry = new Promise((_, reject) => {
22122
+ abort = () => {
22123
+ controller.abort();
22124
+ reject(unavailable());
22125
+ };
22126
+ timer = setTimeout(abort, TIMEOUT_MS);
22127
+ parent?.addEventListener("abort", abort, { once: true });
22128
+ if (parent?.aborted)
22129
+ abort();
22130
+ });
22131
+ try {
22132
+ return await Promise.race([work(controller.signal), expiry]);
22133
+ } finally {
22134
+ clearTimeout(timer);
22135
+ parent?.removeEventListener("abort", abort);
22136
+ }
22137
+ }
22138
+ async function result(response) {
22139
+ try {
22140
+ const bytes = await boundedBytes(response.body, RESULT_BYTES);
22141
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
22142
+ } catch {
22143
+ throw unavailable();
22144
+ }
22145
+ }
22146
+ async function send(suffix, init, descriptor, initial, parent) {
22147
+ const original = initial ?? await resolveFilesContext();
22148
+ const checkCurrent = () => {
22149
+ if (parent?.aborted)
22150
+ throw unavailable();
22151
+ if (!original.isCurrent())
22152
+ throw new FileOperationError("unsupported_context", 400);
22153
+ };
22154
+ for (let attempt = 0;attempt < ATTEMPTS; attempt++) {
22155
+ checkCurrent();
22156
+ const context = attempt === 0 && !initial ? original : await resolveFilesContext();
22157
+ checkCurrent();
22158
+ if (context.url !== original.url)
22159
+ throw new FileOperationError("unsupported_context", 400);
22160
+ try {
22161
+ return await timed(async (signal) => {
22162
+ const response = await fetch(`${context.url}${suffix}`, {
22163
+ ...init,
22164
+ signal,
22165
+ headers: { ...context.headers, ...init.headers },
22166
+ redirect: "error",
22167
+ cache: "no-store"
22168
+ });
22169
+ if (!response.ok) {
22170
+ const value = await result(response);
22171
+ const error = value && typeof value === "object" && "error" in value ? value.error : undefined;
22172
+ const received = error && typeof error === "object" && "code" in error ? error.code : undefined;
22173
+ const code = failures[response.status]?.find((code2) => code2 === received);
22174
+ throw code ? new FileOperationError(code, response.status, code === "capacity" || code === "unavailable") : unavailable();
22175
+ }
22176
+ if (response.status !== 200)
22177
+ throw unavailable();
22178
+ checkCurrent();
22179
+ if (!descriptor)
22180
+ return response;
22181
+ const file = parseFileDescriptor(await result(response));
22182
+ if (file.size > MAX_BYTES)
22183
+ throw unavailable();
22184
+ checkCurrent();
22185
+ return file;
22186
+ }, parent);
22187
+ } catch (error) {
22188
+ const failure = error instanceof FileOperationError ? error : unavailable();
22189
+ if (!failure.retryable || attempt + 1 === ATTEMPTS)
22190
+ throw failure;
22191
+ }
22192
+ }
22193
+ throw unavailable();
22194
+ }
22195
+ function idPath(id2) {
22196
+ if (typeof id2 !== "string" || !FILE_ID_PATTERN.test(id2))
22197
+ throw invalid();
22198
+ return `/${id2}`;
22199
+ }
22200
+ var files = {
22201
+ async create(input, options) {
22202
+ const context = await resolveFilesContext();
22203
+ 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")))
22204
+ throw invalid();
22205
+ const blob = input.file instanceof Blob;
22206
+ const array2 = input.file instanceof Uint8Array;
22207
+ const stream = input.file instanceof ReadableStream;
22208
+ if (!blob && !array2 && !stream)
22209
+ throw invalid();
22210
+ const intrinsic = blob ? input.file.size : array2 ? input.file.byteLength : undefined;
22211
+ if (intrinsic !== undefined && input.size !== undefined && input.size !== intrinsic)
22212
+ throw invalid();
22213
+ let name = input.name;
22214
+ if (name === undefined && blob && "name" in input.file && typeof input.file.name === "string") {
22215
+ name = typeof globalThis.File !== "undefined" && input.file instanceof globalThis.File ? input.file.name : input.file.name.split(/[\\/]/).at(-1);
22216
+ }
22217
+ const metadata = parseFileCreateMetadata({
22218
+ name,
22219
+ size: intrinsic ?? input.size,
22220
+ path: input.path,
22221
+ storage: input.storage,
22222
+ csv: input.csv,
22223
+ requestKey: options?.requestKey === undefined ? crypto.randomUUID() : options.requestKey
22224
+ }, MAX_BYTES);
22225
+ const header = encodeURIComponent(JSON.stringify(metadata));
22226
+ if (new TextEncoder().encode(header).byteLength > METADATA_BYTES)
22227
+ throw invalid();
22228
+ if (active >= MAX_ACTIVE)
22229
+ throw new FileOperationError("capacity", 429, true);
22230
+ active++;
22231
+ return timed(async (signal) => {
22232
+ try {
22233
+ const bytes = array2 ? input.file.slice() : await boundedBytes(blob ? input.file.stream() : input.file, metadata.size);
22234
+ if (bytes.byteLength !== metadata.size)
22235
+ throw invalid();
22236
+ if (signal.aborted)
22237
+ throw unavailable();
22238
+ return await send("", {
22239
+ method: "POST",
22240
+ headers: { "content-type": "application/octet-stream", "x-oddington-file": header },
22241
+ body: bytes.buffer
22242
+ }, true, context, signal);
22243
+ } finally {
22244
+ active--;
22245
+ }
22246
+ }).catch((error) => {
22247
+ throw error instanceof FileOperationError ? error : unavailable();
22248
+ });
22249
+ },
22250
+ async get(id2) {
22251
+ return send(idPath(id2), { method: "GET" }, true);
22252
+ },
22253
+ async read(id2) {
22254
+ return send(`${idPath(id2)}/content`, { method: "GET" }, false);
22255
+ }
22256
+ };
21859
22257
  // src/os.ts
21860
22258
  var installedDispose;
21861
22259
  function init({ auth }) {
@@ -21870,7 +22268,7 @@ function init({ auth }) {
21870
22268
  workspace = slug;
21871
22269
  }
21872
22270
  } catch {}
21873
- let active = true;
22271
+ let active2 = true;
21874
22272
  let revision = 0;
21875
22273
  let person;
21876
22274
  const { data: { subscription } } = auth.onAuthStateChange((event, session) => {
@@ -21880,11 +22278,11 @@ function init({ auth }) {
21880
22278
  person = next;
21881
22279
  });
21882
22280
  const resolve = async () => {
21883
- if (!active || !endpoint || !workspace)
22281
+ if (!active2 || !endpoint || !workspace)
21884
22282
  throw new Error("Hosted Data context unavailable.");
21885
22283
  const started = revision;
21886
22284
  const { data: { session }, error } = await auth.getSession();
21887
- if (!active || started !== revision || error || !session || person !== undefined && person !== session.user.id) {
22285
+ if (!active2 || started !== revision || error || !session || person !== undefined && person !== session.user.id) {
21888
22286
  throw new Error("Hosted Data context unavailable.");
21889
22287
  }
21890
22288
  const expiresAt = (session.expires_at ?? 0) * 1000;
@@ -21892,13 +22290,19 @@ function init({ auth }) {
21892
22290
  throw new Error("Hosted Data context unavailable.");
21893
22291
  }
21894
22292
  person = session.user.id;
21895
- return { workspace, endpoint, accessToken: session.access_token, expiresAt };
22293
+ return {
22294
+ workspace,
22295
+ endpoint,
22296
+ accessToken: session.access_token,
22297
+ expiresAt,
22298
+ isCurrent: () => active2 && revision === started && person === session.user.id
22299
+ };
21896
22300
  };
21897
22301
  const uninstall = installMemberContext(resolve);
21898
22302
  const dispose = () => {
21899
- if (!active)
22303
+ if (!active2)
21900
22304
  return;
21901
- active = false;
22305
+ active2 = false;
21902
22306
  revision++;
21903
22307
  subscription.unsubscribe();
21904
22308
  uninstall();
@@ -21932,7 +22336,7 @@ function routinePublicationClient(options = {}) {
21932
22336
  } : async () => {
21933
22337
  throw new Error("Member publication requires a host-supplied request");
21934
22338
  });
21935
- async function send(operation, body) {
22339
+ async function send2(operation, body) {
21936
22340
  if (local)
21937
22341
  return await local(operation, body);
21938
22342
  const response = await request(`/api/cli/workspaces/${workspace}/inbox/${operation}`, {
@@ -21940,23 +22344,23 @@ function routinePublicationClient(options = {}) {
21940
22344
  headers: { "content-type": "application/json", ...headers },
21941
22345
  body: JSON.stringify(body)
21942
22346
  });
21943
- const result = await response.json();
22347
+ const result2 = await response.json();
21944
22348
  if (!response.ok && operation === "records/update")
21945
- throw new RoutineRecordWriteError(result.error ?? "Could not confirm routine preparation", result.effect ?? "uncertain");
22349
+ throw new RoutineRecordWriteError(result2.error ?? "Could not confirm routine preparation", result2.effect ?? "uncertain");
21946
22350
  if (!response.ok)
21947
- throw new Error(result.error ?? "Could not confirm routine publication");
21948
- return result;
22351
+ throw new Error(result2.error ?? "Could not confirm routine publication");
22352
+ return result2;
21949
22353
  }
21950
22354
  const transport = {
21951
22355
  async publish(body) {
21952
- return await send("publish", body);
22356
+ return await send2("publish", body);
21953
22357
  },
21954
22358
  async resolve(body) {
21955
- await send("resolve", body);
22359
+ await send2("resolve", body);
21956
22360
  }
21957
22361
  };
21958
22362
  async function readRecords(input) {
21959
- return await send("records/read", validateRecordRead(input));
22363
+ return await send2("records/read", validateRecordRead(input));
21960
22364
  }
21961
22365
  return {
21962
22366
  ...createInboxPublisher(transport),
@@ -21966,17 +22370,17 @@ function routinePublicationClient(options = {}) {
21966
22370
  async writeRecords(input, options2) {
21967
22371
  if (!capability)
21968
22372
  throw new Error("A trusted routine launch is required");
21969
- return await send("records/update", validateRoutineRecordWrite({ input, requestKey: options2?.requestKey }));
22373
+ return await send2("records/update", validateRoutineRecordWrite({ input, requestKey: options2?.requestKey }));
21970
22374
  },
21971
22375
  async updateRecords(input, options2) {
21972
22376
  if (!capability)
21973
22377
  throw new Error("A trusted routine launch is required");
21974
- return await send("records/update", validateRoutineRecordWrite({ input: validateRecordUpdate(input), requestKey: options2?.requestKey }));
22378
+ return await send2("records/update", validateRoutineRecordWrite({ input: validateRecordUpdate(input), requestKey: options2?.requestKey }));
21975
22379
  },
21976
22380
  async deleteRecords(input, options2) {
21977
22381
  if (!capability)
21978
22382
  throw new Error("A trusted routine launch is required");
21979
- return await send("records/update", validateRoutineRecordWrite({ input: validateRecordDelete(input), requestKey: options2?.requestKey }));
22383
+ return await send2("records/update", validateRoutineRecordWrite({ input: validateRecordDelete(input), requestKey: options2?.requestKey }));
21980
22384
  }
21981
22385
  },
21982
22386
  files: {
@@ -21998,14 +22402,14 @@ function routinePublicationClient(options = {}) {
21998
22402
  },
21999
22403
  body: Uint8Array.from(bytes).buffer
22000
22404
  });
22001
- const result = await response.json();
22405
+ const result2 = await response.json();
22002
22406
  if (!response.ok)
22003
- throw new Error(result.error ?? "Could not retain file");
22004
- return await verifyInboxFile(bytes, validateInboxFile(result));
22407
+ throw new Error(result2.error ?? "Could not retain file");
22408
+ return await verifyInboxFile(bytes, validateInboxFile(result2));
22005
22409
  }
22006
22410
  },
22007
22411
  async check(body) {
22008
- await send("check", { requestKey: body.requestKey, item: validatePublication(body.item) });
22412
+ await send2("check", { requestKey: body.requestKey, item: validatePublication(body.item) });
22009
22413
  }
22010
22414
  };
22011
22415
  }
@@ -22019,6 +22423,7 @@ export {
22019
22423
  recordBlock,
22020
22424
  question,
22021
22425
  os,
22426
+ files,
22022
22427
  fileBlock,
22023
22428
  defineHandler,
22024
22429
  data,
@@ -22027,6 +22432,7 @@ export {
22027
22432
  asksBlock,
22028
22433
  RoutineRecordWriteError,
22029
22434
  InboxContractError,
22435
+ FileOperationError,
22030
22436
  DataOperationError,
22031
22437
  AirtableOperationError
22032
22438
  };
@@ -13,10 +13,17 @@ 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 } 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';
17
17
  export declare const data: {
18
18
  repository(name: string): import('../../../shared/data-contract.js').DataRepository;
19
19
  };
20
+ export { FileOperationError } from '../../../shared/files-contract.js';
21
+ export type { File, FileInput, FileStorage, FileCreateInput, FileCreateOptions, FileErrorCode } from '../../../shared/files-contract.js';
22
+ export declare const files: {
23
+ create(input: import('../../../shared/files-contract.js').FileCreateInput, options?: import('../../../shared/files-contract.js').FileCreateOptions): Promise<import('../../../shared/files-contract.js').File>;
24
+ get(id: string): Promise<import('../../../shared/files-contract.js').File>;
25
+ read(id: string): Promise<Response>;
26
+ };
20
27
  /** The host's existing refreshing session source; supplies identity, never membership. */
21
28
  export type HostedSessionSource = {
22
29
  getSession(): Promise<{
@@ -44,10 +44,38 @@ export type CollectionSchema = CollectionSummary & {
44
44
  fields: PublicField[];
45
45
  };
46
46
  export type QueryScalar = string | number | boolean | null;
47
+ type LowerBound = {
48
+ $gt: number | string;
49
+ $gte?: never;
50
+ } | {
51
+ $gte: number | string;
52
+ $gt?: never;
53
+ };
54
+ type UpperBound = {
55
+ $lt: number | string;
56
+ $lte?: never;
57
+ } | {
58
+ $lte: number | string;
59
+ $lt?: never;
60
+ };
61
+ export type RangeCondition = ((LowerBound & {
62
+ $lt?: never;
63
+ $lte?: never;
64
+ }) | (UpperBound & {
65
+ $gt?: never;
66
+ $gte?: never;
67
+ }) | (LowerBound & UpperBound)) & {
68
+ $in?: never;
69
+ };
70
+ export type QueryCondition = QueryScalar | RangeCondition | {
71
+ $in: QueryScalar[];
72
+ $gt?: never;
73
+ $gte?: never;
74
+ $lt?: never;
75
+ $lte?: never;
76
+ };
47
77
  export type SelectionOptions = {
48
- where?: Record<string, QueryScalar | {
49
- $in: QueryScalar[];
50
- }>;
78
+ where?: Record<string, QueryCondition>;
51
79
  fields?: string[];
52
80
  order?: Record<string, 'asc' | 'desc'>;
53
81
  limit?: number;
@@ -78,6 +106,19 @@ export type RecordsResult = {
78
106
  $meta: SelectionMetadata;
79
107
  [collection: string]: SelectedRecord[] | SelectionMetadata;
80
108
  };
109
+ export type RecordIdentity = Pick<SelectedRecord, '$ref' | '$version'>;
110
+ export type RecordUpdateSet = Record<string, DataValue>;
111
+ export type DataPublication = {
112
+ version: string;
113
+ created: [];
114
+ updated: RecordIdentity[];
115
+ deleted: [];
116
+ };
117
+ export interface DataChangeSet {
118
+ update(record: RecordIdentity, set: RecordUpdateSet): void;
119
+ validate(): Promise<void>;
120
+ publish(): Promise<DataPublication>;
121
+ }
81
122
  export interface DataRepository {
82
123
  collections: {
83
124
  list(): Promise<CollectionSummary[]>;
@@ -86,10 +127,14 @@ export interface DataRepository {
86
127
  records: {
87
128
  select(query: RecordsQuery): Promise<RecordsResult>;
88
129
  };
130
+ changes(): DataChangeSet;
89
131
  }
90
- export type DataErrorCode = 'context_unavailable' | 'unauthenticated' | 'access_denied' | 'invalid_query' | 'request_too_large' | 'capacity' | 'unavailable';
132
+ export type DataErrorCode = 'context_unavailable' | 'unauthenticated' | 'access_denied' | 'invalid_query' | 'request_too_large' | 'capacity' | 'unavailable' | 'invalid_change' | 'conflict' | 'publication_pending';
133
+ export type DataEffect = 'unapplied' | 'uncertain' | 'applied';
91
134
  export declare class DataOperationError extends Error {
92
135
  readonly code: DataErrorCode;
93
136
  readonly retryable: boolean;
94
- constructor(code: DataErrorCode, retryable?: boolean);
137
+ readonly effect?: DataEffect | undefined;
138
+ constructor(code: DataErrorCode, retryable?: boolean, effect?: DataEffect | undefined);
95
139
  }
140
+ export {};
@@ -0,0 +1,66 @@
1
+ /** Provider-neutral authored contract. Registries and object locations stay server-side. */
2
+ export type FileStorage = 'local' | 'remote' | 'both';
3
+ export type FileInput = Blob | Uint8Array | ReadableStream<Uint8Array>;
4
+ export type FileCsvHints = {
5
+ encoding?: 'utf-8' | 'windows-1252';
6
+ delimiter?: ',' | ';' | '\t';
7
+ headerRow?: number;
8
+ };
9
+ export type FileCreateInput = {
10
+ file: FileInput;
11
+ size?: number;
12
+ path?: string;
13
+ name?: string;
14
+ storage?: FileStorage;
15
+ csv?: FileCsvHints;
16
+ };
17
+ export type FileCreateOptions = {
18
+ requestKey?: string;
19
+ };
20
+ export type File = {
21
+ id: string;
22
+ name: string;
23
+ path: string;
24
+ mediaType: string;
25
+ size: number;
26
+ sha256: string;
27
+ enrichments: Record<string, {
28
+ mediaType: string;
29
+ size: number;
30
+ sha256: string;
31
+ }>;
32
+ details: {
33
+ kind: 'csv';
34
+ encoding: 'utf-8' | 'windows-1252';
35
+ delimiter: ',' | ';' | '\t';
36
+ headerRow: number;
37
+ dataRows: number;
38
+ } | {
39
+ kind: 'pdf';
40
+ pages?: number;
41
+ } | {
42
+ kind: 'other';
43
+ };
44
+ };
45
+ export type FileErrorCode = 'invalid_input' | 'unsupported_context' | 'unauthenticated' | 'access_denied' | 'request_too_large' | 'conflict' | 'integrity' | 'unavailable' | 'capacity' | 'method_not_allowed';
46
+ export declare class FileOperationError extends Error {
47
+ readonly code: FileErrorCode;
48
+ readonly status: number;
49
+ readonly retryable: boolean;
50
+ constructor(code: FileErrorCode, status: number, retryable?: boolean);
51
+ }
52
+ export declare const FILE_ID_PATTERN: RegExp;
53
+ /** Treat descriptor transport as untrusted; no provider fields escape the boundary. */
54
+ export declare function parseFileDescriptor(value: unknown): File;
55
+ export declare function safeFileName(value: unknown): value is string;
56
+ export declare function safeFilePath(value: unknown): value is string;
57
+ /** Private transport metadata: normalized material options exclude the request key and size declaration. */
58
+ export type FileCreateMetadata = {
59
+ name: string;
60
+ path: string;
61
+ storage: 'remote';
62
+ csv?: Required<FileCsvHints>;
63
+ size: number;
64
+ requestKey: string;
65
+ };
66
+ export declare function parseFileCreateMetadata(value: unknown, maxBytes: number): FileCreateMetadata;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oddington/sdk",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Oddington routine and response-handler SDK",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",