@oddington/sdk 0.1.0 → 0.1.2

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
@@ -13,6 +13,37 @@ import { defineHandler, resolveAsk, routinePublicationClient } from '@oddington/
13
13
  const inbox = routinePublicationClient();
14
14
  ```
15
15
 
16
+ Bind a declared Airtable connection with `connections.airtable('project-management')`
17
+ from the package root. Import and binding perform no network or credential work.
18
+ The client exposes `tables.list()` and `records.list`, `get`, `create`, `update`
19
+ and `delete`. Reads allow table names or stable IDs and return all ordered pages;
20
+ writes require a stable table ID and one nonempty batch of at most ten records.
21
+ Operations cannot select another workspace, base, token or provider URL.
22
+
23
+ ```ts
24
+ import { connections } from '@oddington/sdk';
25
+
26
+ const airtable = connections.airtable('project-management');
27
+ const changed = await airtable.records.update({ table: 'tblContacts', records: [{
28
+ id: 'recRetained', expected: { Email: 'james@example.com', Company: null },
29
+ set: { Company: ['recCompany'] },
30
+ }] });
31
+ ```
32
+
33
+ Preserve original reviewed `expected` values. Non-null values must match exactly;
34
+ `null` requires an omitted field key and cannot be used as an update value. Reads
35
+ preserve Airtable's omitted-field shape. Create requires nonempty `fields`, update
36
+ requires nonempty `set`, and untouched fields remain unchanged. Guarded methods
37
+ read before a single batch write; this leaves a concurrency race and provides no
38
+ atomic compare-and-swap, rollback or implicit retry.
39
+
40
+ Success returns a safe Airtable `receipt` with the connection, configured base,
41
+ table, operation and acknowledged IDs. Create/update also return acknowledged
42
+ records separately. `AirtableOperationError` reports `unapplied`, `partial` or
43
+ `uncertain` effects and known safe `receipts`. Stop after partial or uncertain
44
+ work; never repeat the whole change blindly. Credentials must never be retained
45
+ in files, publication, logs, handler input, errors or receipts.
46
+
16
47
  Managed routine launches supply workspace, scoped capability and working root.
17
48
  The defaults read `ODDINGTON_ROUTINE_WORKSPACE`, `ODDINGTON_ROUTINE_RUN` and
18
49
  `ODDINGTON_INBOX_WORKING_ROOT` (or the current directory). Callers may supply the
@@ -59,11 +90,3 @@ installation. Customer manifests cannot replace the runner-owned SDK. The runner
59
90
  selects the approved code revision and handler, and supplies only scoped capability
60
91
  transport. The package contains public runtime code, declarations, this README and
61
92
  its license; it has no executable, operator/server code or infrastructure credentials.
62
-
63
- From the product checkout, run `bun run --filter @oddington/sdk build`, then
64
- `bun run --filter @oddington/sdk verify:package`. Verification installs the actual
65
- tarball offline into a temporary consumer without the CLI, imports and typechecks
66
- through `@oddington/sdk`, and exercises retained evidence, handlerless publication
67
- and guarded repository calls. Set `PACKAGE_VERIFY_NODE` to the Node 22.12.0
68
- executable for supported-baseline proof. The CLI's corresponding verifier installs
69
- both local tarballs, checks `os --help` and refuses the removed subpath.
package/dist/index.js CHANGED
@@ -7202,9 +7202,9 @@ var require_PDFContext = __commonJS((exports) => {
7202
7202
  return array2;
7203
7203
  } else {
7204
7204
  var dict = PDFDict_1.default.withContext(this);
7205
- var keys2 = Object.keys(literal);
7206
- for (var idx = 0, len = keys2.length;idx < len; idx++) {
7207
- var key = keys2[idx];
7205
+ var keys3 = Object.keys(literal);
7206
+ for (var idx = 0, len = keys3.length;idx < len; idx++) {
7207
+ var key = keys3[idx];
7208
7208
  var value = literal[key];
7209
7209
  if (value !== undefined)
7210
7210
  dict.set(PDFName_1.default.of(key), this.obj(value));
@@ -13306,9 +13306,9 @@ var require_PDFWidgetAnnotation = __commonJS((exports) => {
13306
13306
  var _a;
13307
13307
  var normal = (_a = this.getAppearances()) === null || _a === undefined ? undefined : _a.normal;
13308
13308
  if (normal instanceof PDFDict_1.default) {
13309
- var keys2 = normal.keys();
13310
- for (var idx = 0, len = keys2.length;idx < len; idx++) {
13311
- var key = keys2[idx];
13309
+ var keys3 = normal.keys();
13310
+ for (var idx = 0, len = keys3.length;idx < len; idx++) {
13311
+ var key = keys3[idx];
13312
13312
  if (key !== PDFName_1.default.of("Off"))
13313
13313
  return key;
13314
13314
  }
@@ -14091,28 +14091,28 @@ var require_PDFAcroForm = __commonJS((exports) => {
14091
14091
  this.dict = dict;
14092
14092
  }
14093
14093
  PDFAcroForm2.prototype.Fields = function() {
14094
- var fields2 = this.dict.lookup(PDFName_1.default.of("Fields"));
14095
- if (fields2 instanceof PDFArray_1.default)
14096
- return fields2;
14094
+ var fields3 = this.dict.lookup(PDFName_1.default.of("Fields"));
14095
+ if (fields3 instanceof PDFArray_1.default)
14096
+ return fields3;
14097
14097
  return;
14098
14098
  };
14099
14099
  PDFAcroForm2.prototype.getFields = function() {
14100
14100
  var Fields = this.normalizedEntries().Fields;
14101
- var fields2 = new Array(Fields.size());
14101
+ var fields3 = new Array(Fields.size());
14102
14102
  for (var idx = 0, len = Fields.size();idx < len; idx++) {
14103
14103
  var ref = Fields.get(idx);
14104
14104
  var dict = Fields.lookup(idx, PDFDict_1.default);
14105
- fields2[idx] = [utils_1.createPDFAcroField(dict, ref), ref];
14105
+ fields3[idx] = [utils_1.createPDFAcroField(dict, ref), ref];
14106
14106
  }
14107
- return fields2;
14107
+ return fields3;
14108
14108
  };
14109
14109
  PDFAcroForm2.prototype.getAllFields = function() {
14110
14110
  var allFields = [];
14111
- var pushFields = function(fields2) {
14112
- if (!fields2)
14111
+ var pushFields = function(fields3) {
14112
+ if (!fields3)
14113
14113
  return;
14114
- for (var idx = 0, len = fields2.length;idx < len; idx++) {
14115
- var field = fields2[idx];
14114
+ for (var idx = 0, len = fields3.length;idx < len; idx++) {
14115
+ var field = fields3[idx];
14116
14116
  allFields.push(field);
14117
14117
  var fieldModel = field[0];
14118
14118
  if (fieldModel instanceof PDFAcroNonTerminal_1.default) {
@@ -14129,13 +14129,13 @@ var require_PDFAcroForm = __commonJS((exports) => {
14129
14129
  };
14130
14130
  PDFAcroForm2.prototype.removeField = function(field) {
14131
14131
  var parent = field.getParent();
14132
- var fields2 = parent === undefined ? this.normalizedEntries().Fields : parent.Kids();
14133
- var index = fields2 === null || fields2 === undefined ? undefined : fields2.indexOf(field.ref);
14134
- if (fields2 === undefined || index === undefined) {
14132
+ var fields3 = parent === undefined ? this.normalizedEntries().Fields : parent.Kids();
14133
+ var index = fields3 === null || fields3 === undefined ? undefined : fields3.indexOf(field.ref);
14134
+ if (fields3 === undefined || index === undefined) {
14135
14135
  throw new Error("Tried to remove inexistent field " + field.getFullyQualifiedName());
14136
14136
  }
14137
- fields2.remove(index);
14138
- if (parent !== undefined && fields2.size() === 0) {
14137
+ fields3.remove(index);
14138
+ if (parent !== undefined && fields3.size() === 0) {
14139
14139
  this.removeField(parent);
14140
14140
  }
14141
14141
  };
@@ -19086,20 +19086,20 @@ var require_PDFForm = __commonJS((exports) => {
19086
19086
  };
19087
19087
  PDFForm2.prototype.getFields = function() {
19088
19088
  var allFields = this.acroForm.getAllFields();
19089
- var fields2 = [];
19089
+ var fields3 = [];
19090
19090
  for (var idx = 0, len = allFields.length;idx < len; idx++) {
19091
19091
  var _a = allFields[idx], acroField = _a[0], ref = _a[1];
19092
19092
  var field = convertToPDFField(acroField, ref, this.doc);
19093
19093
  if (field)
19094
- fields2.push(field);
19094
+ fields3.push(field);
19095
19095
  }
19096
- return fields2;
19096
+ return fields3;
19097
19097
  };
19098
19098
  PDFForm2.prototype.getFieldMaybe = function(name) {
19099
19099
  utils_1.assertIs(name, "name", ["string"]);
19100
- var fields2 = this.getFields();
19101
- for (var idx = 0, len = fields2.length;idx < len; idx++) {
19102
- var field = fields2[idx];
19100
+ var fields3 = this.getFields();
19101
+ for (var idx = 0, len = fields3.length;idx < len; idx++) {
19102
+ var field = fields3[idx];
19103
19103
  if (field.getName() === name)
19104
19104
  return field;
19105
19105
  }
@@ -19222,9 +19222,9 @@ var require_PDFForm = __commonJS((exports) => {
19222
19222
  if (options.updateFieldAppearances) {
19223
19223
  this.updateFieldAppearances();
19224
19224
  }
19225
- var fields2 = this.getFields();
19226
- for (var i = 0, lenFields = fields2.length;i < lenFields; i++) {
19227
- var field = fields2[i];
19225
+ var fields3 = this.getFields();
19226
+ for (var i = 0, lenFields = fields3.length;i < lenFields; i++) {
19227
+ var field = fields3[i];
19228
19228
  var widgets = field.acroField.getWidgets();
19229
19229
  for (var j = 0, lenWidgets = widgets.length;j < lenWidgets; j++) {
19230
19230
  var widget = widgets[j];
@@ -19271,9 +19271,9 @@ var require_PDFForm = __commonJS((exports) => {
19271
19271
  PDFForm2.prototype.updateFieldAppearances = function(font) {
19272
19272
  utils_1.assertOrUndefined(font, "font", [[PDFFont_1.default, "PDFFont"]]);
19273
19273
  font = font !== null && font !== undefined ? font : this.getDefaultFont();
19274
- var fields2 = this.getFields();
19275
- for (var idx = 0, len = fields2.length;idx < len; idx++) {
19276
- var field = fields2[idx];
19274
+ var fields3 = this.getFields();
19275
+ for (var idx = 0, len = fields3.length;idx < len; idx++) {
19276
+ var field = fields3[idx];
19277
19277
  if (field.needsAppearancesUpdate()) {
19278
19278
  field.defaultUpdateAppearances(font);
19279
19279
  }
@@ -19351,9 +19351,9 @@ var require_PDFForm = __commonJS((exports) => {
19351
19351
  return nonTerminal;
19352
19352
  };
19353
19353
  PDFForm2.prototype.findNonTerminal = function(partialName, parent) {
19354
- var fields2 = parent instanceof core_1.PDFAcroForm ? this.acroForm.getFields() : core_1.createPDFAcroFields(parent.Kids());
19355
- for (var idx = 0, len = fields2.length;idx < len; idx++) {
19356
- var _a = fields2[idx], field = _a[0], ref = _a[1];
19354
+ var fields3 = parent instanceof core_1.PDFAcroForm ? this.acroForm.getFields() : core_1.createPDFAcroFields(parent.Kids());
19355
+ for (var idx = 0, len = fields3.length;idx < len; idx++) {
19356
+ var _a = fields3[idx], field = _a[0], ref = _a[1];
19357
19357
  if (field.getPartialName() === partialName) {
19358
19358
  if (field instanceof core_1.PDFAcroNonTerminal)
19359
19359
  return [field, ref];
@@ -19408,9 +19408,9 @@ var require_PDFForm = __commonJS((exports) => {
19408
19408
  var parent = _a[0], parentRef = _a[1];
19409
19409
  var field = _b[0], fieldRef = _b[1];
19410
19410
  var entries = parent.normalizedEntries();
19411
- var fields2 = core_1.createPDFAcroFields("Kids" in entries ? entries.Kids : entries.Fields);
19412
- for (var idx = 0, len = fields2.length;idx < len; idx++) {
19413
- if (fields2[idx][0].getPartialName() === partialName) {
19411
+ var fields3 = core_1.createPDFAcroFields("Kids" in entries ? entries.Kids : entries.Fields);
19412
+ for (var idx = 0, len = fields3.length;idx < len; idx++) {
19413
+ if (fields3[idx][0].getPartialName() === partialName) {
19414
19414
  throw new errors_1.FieldAlreadyExistsError(partialName);
19415
19415
  }
19416
19416
  }
@@ -21549,27 +21549,6 @@ function validateResponseDefinition(value) {
21549
21549
  return fail("Unsupported option source");
21550
21550
  }
21551
21551
  }
21552
- function validateResponse(definition, value) {
21553
- const saved = validateResponseDefinition(definition);
21554
- const response = object(value, "response");
21555
- if (saved.source.type === "records") {
21556
- keys(response, ["selection"], "response");
21557
- const record = validateRecordReference(response.selection);
21558
- if (record.repository !== saved.source.collection.repository)
21559
- fail("Record belongs to another repository");
21560
- return { selection: record };
21561
- }
21562
- keys(response, ["selection", "note"], "response");
21563
- const choice = id(response.selection, "response selection");
21564
- const option = saved.source.options.find((option2) => option2.value === choice);
21565
- if (!option)
21566
- fail("Choice was not published");
21567
- if (option.note)
21568
- return { selection: choice, note: text(response.note, "required note").trim() };
21569
- if (Object.hasOwn(response, "note"))
21570
- fail("This choice does not accept a note");
21571
- return { selection: choice };
21572
- }
21573
21552
  function handlerReference(value) {
21574
21553
  const source = object(value, "handler reference");
21575
21554
  keys(source, ["id", "input"], "handler reference");
@@ -21662,27 +21641,10 @@ function validatePublication(value) {
21662
21641
  recipients: [...new Set([...extra, ...questions.flatMap((entry) => [entry.to, ...entry.allowedResponders])])]
21663
21642
  };
21664
21643
  }
21665
- function recipientPresentation(value) {
21666
- const publication = validatePublication(value);
21667
- if (publication.kind === "digest")
21668
- return publication;
21669
- return { ...publication, questions: publication.questions.map(({ handler: _private, ...question2 }) => question2) };
21670
- }
21671
- function oddington(value) {
21672
- const source = object(value, "Oddington assignment");
21673
- keys(source, ["reason"], "Oddington assignment");
21674
- return { type: "oddington", reason: text(source.reason, "Oddington reason") };
21675
- }
21676
21644
  function continueAsk(value) {
21677
21645
  const source = object(value, "continueAsk");
21678
- keys(source, ["assignTo"], "continueAsk");
21679
- if (source.assignTo === undefined)
21680
- return { type: "continue" };
21681
- const assignment = object(source.assignTo, "assignment");
21682
- keys(assignment, ["type", "reason"], "assignment");
21683
- if (assignment.type !== "oddington")
21684
- fail("Only Oddington assignment is supported");
21685
- return { type: "continue", assignTo: oddington({ reason: assignment.reason }) };
21646
+ keys(source, [], "continueAsk");
21647
+ return { type: "continue" };
21686
21648
  }
21687
21649
  function resolution(value) {
21688
21650
  const source = object(value, "resolution");
@@ -21694,23 +21656,6 @@ function resolution(value) {
21694
21656
  function resolveAsk(value) {
21695
21657
  return { type: "resolve", ...resolution(value) };
21696
21658
  }
21697
- function validateHandlerTransition(value, state) {
21698
- const source = object(value, "transition");
21699
- if (source.type === "resolve") {
21700
- keys(source, ["type", "outcome", "evidence"], "transition");
21701
- return resolveAsk({
21702
- outcome: source.outcome,
21703
- ...source.evidence === undefined ? {} : { evidence: source.evidence }
21704
- });
21705
- }
21706
- if (source.type !== "continue")
21707
- fail("Unsupported handler transition");
21708
- keys(source, ["type", "assignTo"], "transition");
21709
- const transition = continueAsk(source.assignTo === undefined ? {} : { assignTo: source.assignTo });
21710
- if (transition.type === "continue" && !transition.assignTo && !state.hasOtherViableWork)
21711
- fail("continueAsk would strand the ask without viable work");
21712
- return transition;
21713
- }
21714
21659
  function validateResolution(value) {
21715
21660
  const source = object(value, "published resolution");
21716
21661
  keys(source, ["expectedRevision", "outcome", "evidence"], "published resolution");
@@ -21842,146 +21787,340 @@ function validateRoutineRecordWrite(value) {
21842
21787
  function validCreatePath(value) {
21843
21788
  return value === value.trim() && value.endsWith(".md") && !value.startsWith("/") && value.split("/").every((segment) => !!segment && ![".", ".."].includes(segment) && /^[a-zA-Z0-9_.-]+$/.test(segment));
21844
21789
  }
21845
- // ../../shared/inbox-views.ts
21846
- function validateCapturedRecordTableInput(value) {
21847
- const input = validateRecordTableInput(value);
21848
- if (!input.repository)
21849
- fail2("Repository capture is required");
21850
- return input;
21790
+ // ../../shared/connections.ts
21791
+ function validGoogleScopes(value) {
21792
+ return Array.isArray(value) && value.length > 0 && value.length <= 32 && value.every((scope) => typeof scope === "string" && scope.length <= 256 && /^(https:\/\/www\.googleapis\.com\/auth\/[A-Za-z0-9._-]+|https:\/\/mail\.google\.com\/|openid|email|profile)$/.test(scope)) && new Set(value).size === value.length;
21851
21793
  }
21852
- var commonViews = [
21853
- { name: "common/record", implementation: "common/record@1", workspaces: "*", validate: validateRecordViewInput },
21854
- { name: "common/record-table", implementation: "common/record-table@1", workspaces: "*", validate: validateRecordTableInput }
21855
- ];
21794
+
21795
+ // ../../shared/connection-lease.ts
21796
+ var ROUTINE_CONNECTION_CREDENTIALS_ENV = "ODDINGTON_ROUTINE_CONNECTION_CREDENTIALS";
21797
+ var connectionUuid = (v) => typeof v === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(v);
21798
+ function parseConnectionLease(raw, now = Date.now()) {
21799
+ const v = raw;
21800
+ if (!v || typeof v !== "object" || Array.isArray(v) || !connectionUuid(v.connectionId) || !connectionUuid(v.runId) || typeof v.name !== "string" || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(v.name) || typeof v.accessToken !== "string" || !/^[A-Za-z0-9._~+/-]{1,8192}={0,2}$/.test(v.accessToken))
21801
+ throw new Error("Invalid connection credential");
21802
+ if (v.provider === "gmail") {
21803
+ if (Object.keys(v).sort().join(",") !== "accessToken,accountEmail,connectionId,expiresAt,name,provider,runId,scopes" || typeof v.accountEmail !== "string" || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.accountEmail) || !validGoogleScopes(v.scopes) || typeof v.expiresAt !== "string" || !Number.isFinite(Date.parse(v.expiresAt)) || Date.parse(v.expiresAt) <= now + 60000 || Date.parse(v.expiresAt) > now + 3600000)
21804
+ throw new Error("Invalid connection credential");
21805
+ return v;
21806
+ }
21807
+ if (v.provider === "airtable" && Object.keys(v).sort().join(",") === "accessToken,baseId,connectionId,name,provider,runExpiresAt,runId" && typeof v.baseId === "string" && /^app[A-Za-z0-9]+$/.test(v.baseId) && typeof v.runExpiresAt === "string" && Number.isFinite(Date.parse(v.runExpiresAt)) && Date.parse(v.runExpiresAt) > now + 60000 && Date.parse(v.runExpiresAt) <= now + 15 * 60000)
21808
+ return v;
21809
+ throw new Error("Invalid connection credential");
21810
+ }
21811
+
21812
+ // ../../shared/airtable-contract.ts
21813
+ class AirtableOperationError extends Error {
21814
+ effect;
21815
+ receipts;
21816
+ constructor(message, effect = "unapplied", receipts = []) {
21817
+ super(message);
21818
+ this.effect = effect;
21819
+ this.receipts = receipts;
21820
+ this.name = "AirtableOperationError";
21821
+ }
21822
+ }
21823
+
21824
+ // ../../shared/airtable-client.ts
21825
+ var recordId = (v) => typeof v === "string" && /^rec[A-Za-z0-9]+$/.test(v);
21826
+ var tableId = (v) => typeof v === "string" && /^tbl[A-Za-z0-9]+$/.test(v);
21856
21827
  function fail2(message) {
21857
- throw new InboxContractError(message);
21828
+ throw new AirtableOperationError(message);
21858
21829
  }
21859
- function object2(value, fields2, required, label) {
21860
- const copy = serializable(value);
21861
- if (!copy || typeof copy !== "object" || Array.isArray(copy) || Object.keys(copy).some((key) => !fields2.includes(key)) || required.some((key) => !Object.hasOwn(copy, key)))
21862
- fail2(`Invalid ${label} fields`);
21863
- return copy;
21830
+ function object2(v) {
21831
+ if (!v || typeof v !== "object" || Array.isArray(v) || ![null, Object.prototype].includes(Object.getPrototypeOf(v)))
21832
+ fail2("Invalid Airtable data");
21833
+ return v;
21864
21834
  }
21865
- function text2(value) {
21866
- return typeof value === "string" && !!value.trim() && value === value.trim();
21835
+ function keys2(v, allowed, required = allowed) {
21836
+ if (Object.keys(v).some((k) => !allowed.includes(k)) || required.some((k) => !Object.hasOwn(v, k)))
21837
+ fail2("Invalid Airtable input");
21867
21838
  }
21868
- function readable(value) {
21869
- return typeof value === "string" && !!value.trim();
21839
+ function text2(v) {
21840
+ return typeof v === "string" && v.length > 0 && v.length <= 1024;
21870
21841
  }
21871
- function recordFields(value) {
21872
- if (!Array.isArray(value) || !value.length)
21873
- fail2("Record fields must not be empty");
21874
- const labels = new Set;
21875
- for (const entry of value) {
21876
- const field = object2(entry, ["label", "value"], ["label", "value"], "record field");
21877
- if (!text2(field.label) || labels.has(field.label))
21878
- fail2("Record field labels must be distinct");
21879
- labels.add(field.label);
21880
- recordValue(field.value);
21842
+ function fields2(v, nonempty = false) {
21843
+ const result = object2(v);
21844
+ if (nonempty && !Object.keys(result).length || Object.keys(result).some((k) => !text2(k)))
21845
+ fail2("Invalid Airtable fields");
21846
+ try {
21847
+ return serializable(result);
21848
+ } catch {
21849
+ return fail2("Invalid Airtable fields");
21881
21850
  }
21882
21851
  }
21883
- function recordValue(value) {
21884
- const kind = value && typeof value === "object" ? value.kind : undefined;
21885
- switch (kind) {
21886
- case "text": {
21887
- const v = object2(value, ["kind", "text"], ["kind", "text"], "text value");
21888
- if (!readable(v.text))
21889
- fail2("Record text must be readable");
21890
- break;
21891
- }
21892
- case "missing": {
21893
- const v = object2(value, ["kind", "label"], ["kind", "label"], "missing value");
21894
- if (!text2(v.label))
21895
- fail2("Missing information requires a label");
21896
- break;
21897
- }
21898
- case "record": {
21899
- const v = object2(value, ["kind", "ref", "label", "recordType"], ["kind", "ref", "label"], "record link");
21900
- if (!text2(v.ref) || !text2(v.label) || v.recordType !== undefined && !text2(v.recordType))
21901
- fail2("Invalid supplied record link");
21902
- break;
21852
+ function record(v) {
21853
+ const r = object2(v);
21854
+ if (!recordId(r.id) || r.createdTime !== undefined && (typeof r.createdTime !== "string" || !Number.isFinite(Date.parse(r.createdTime))))
21855
+ fail2("Malformed Airtable record");
21856
+ return { id: r.id, ...r.createdTime === undefined ? {} : { createdTime: r.createdTime }, fields: fields2(r.fields) };
21857
+ }
21858
+ function equal(a, b) {
21859
+ if (a === b)
21860
+ return true;
21861
+ if (!a || !b || typeof a !== "object" || typeof b !== "object" || Array.isArray(a) !== Array.isArray(b))
21862
+ return false;
21863
+ const left = Object.keys(a), right = Object.keys(b);
21864
+ return left.length === right.length && left.every((k) => Object.hasOwn(b, k) && equal(a[k], b[k]));
21865
+ }
21866
+ function createConnections(transport) {
21867
+ return { airtable(name) {
21868
+ if (typeof name !== "string" || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(name))
21869
+ fail2("Invalid Airtable connection name");
21870
+ const call = (operation, input) => transport(name, operation, input);
21871
+ return {
21872
+ tables: { list: async () => await call("tables.list", {}) },
21873
+ records: {
21874
+ list: async (input) => await call("records.list", input),
21875
+ get: async (input) => await call("records.get", input),
21876
+ create: async (input) => await call("records.create", input),
21877
+ update: async (input) => await call("records.update", input),
21878
+ delete: async (input) => await call("records.delete", input)
21879
+ }
21880
+ };
21881
+ } };
21882
+ }
21883
+ async function executeAirtableOperation(authority, operation, raw, options = {}) {
21884
+ const { name, baseId, accessToken } = authority;
21885
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(name) || !/^app[A-Za-z0-9]+$/.test(baseId) || !/^[A-Za-z0-9._~+/-]{1,8192}={0,2}$/.test(accessToken))
21886
+ fail2("Airtable connection unavailable");
21887
+ let input;
21888
+ try {
21889
+ input = object2(serializable(raw));
21890
+ } catch {
21891
+ return fail2("Invalid Airtable input");
21892
+ }
21893
+ const mutation = ["records.create", "records.update", "records.delete"].includes(operation);
21894
+ const receipt = (ids2) => ({
21895
+ provider: "airtable",
21896
+ connection: `airtable/${name}`,
21897
+ baseId,
21898
+ table: input.table,
21899
+ operation: operation.slice(8),
21900
+ records: ids2.map((id2) => ({ id: id2 }))
21901
+ });
21902
+ async function request(path2, init = {}, write = false) {
21903
+ let response;
21904
+ try {
21905
+ response = await (options.fetch ?? fetch)(`https://api.airtable.com/v0/${path2}`, {
21906
+ ...init,
21907
+ headers: { authorization: `Bearer ${accessToken}`, "content-type": "application/json" },
21908
+ redirect: "manual",
21909
+ signal: options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(15000)]) : AbortSignal.timeout(15000)
21910
+ });
21911
+ } catch {
21912
+ throw new AirtableOperationError("Airtable request acknowledgment unavailable", write ? "uncertain" : "unapplied");
21903
21913
  }
21904
- case "list": {
21905
- const v = object2(value, ["kind", "items", "remaining"], ["kind", "items"], "list value");
21906
- if (!Array.isArray(v.items) || !v.items.length || v.remaining !== undefined && (!Number.isSafeInteger(v.remaining) || v.remaining < 0))
21907
- fail2("Invalid Record list or remaining count");
21908
- v.items.forEach(recordValue);
21909
- break;
21914
+ if (!response.ok) {
21915
+ await response.body?.cancel().catch(() => {});
21916
+ throw new AirtableOperationError("Airtable request rejected", write && (response.status >= 500 || response.status === 408 || response.status < 400) ? "uncertain" : "unapplied");
21910
21917
  }
21911
- case "object": {
21912
- const v = object2(value, ["kind", "fields"], ["kind", "fields"], "object value");
21913
- recordFields(v.fields);
21914
- break;
21918
+ try {
21919
+ if (!response.headers.get("content-type")?.toLowerCase().includes("application/json"))
21920
+ throw new Error;
21921
+ const value = object2(await response.json());
21922
+ const safe = serializable(value);
21923
+ if (Object.hasOwn(value, "error") || JSON.stringify(safe).includes(accessToken))
21924
+ throw new Error;
21925
+ return value;
21926
+ } catch {
21927
+ throw new AirtableOperationError("Malformed Airtable acknowledgment", write ? "uncertain" : "unapplied");
21915
21928
  }
21916
- default:
21917
- fail2("Unknown Record value");
21918
21929
  }
21919
- }
21920
- function validateRecordViewInput(value) {
21921
- const v = object2(value, ["record", "related"], ["record"], "Record View");
21922
- if (v.related !== undefined && !Array.isArray(v.related))
21923
- fail2("Related records must be an array");
21924
- const refs = new Set;
21925
- for (const entry of [v.record, ...v.related ?? []]) {
21926
- const r = object2(entry, ["ref", "title", "recordType", "fields", "body"], ["ref", "title", "fields"], "record");
21927
- if (!text2(r.ref) || !text2(r.title) || refs.has(r.ref) || r.recordType !== undefined && !text2(r.recordType))
21928
- fail2("Invalid or duplicate Record reference");
21929
- refs.add(r.ref);
21930
- recordFields(r.fields);
21931
- if (r.body !== undefined) {
21932
- const body = object2(r.body, ["label", "markdown"], ["label", "markdown"], "Record body");
21933
- if (!text2(body.label) || !readable(body.markdown))
21934
- fail2("Invalid Record body");
21935
- }
21930
+ if (operation === "tables.list") {
21931
+ keys2(input, []);
21932
+ const tables = [], seen = new Set;
21933
+ let offset;
21934
+ do {
21935
+ const query2 = new URLSearchParams(offset ? { offset } : {});
21936
+ const page = await request(`meta/bases/${baseId}/tables?${query2}`);
21937
+ if (!Array.isArray(page.tables))
21938
+ fail2("Malformed Airtable tables");
21939
+ for (const value of page.tables) {
21940
+ const t = object2(value);
21941
+ if (!tableId(t.id) || !text2(t.name) || !text2(t.primaryFieldId) || !Array.isArray(t.fields) || tables.some((existing) => existing.id === t.id))
21942
+ fail2("Malformed Airtable table");
21943
+ const metadata = t.fields.map((value2) => {
21944
+ const f = object2(value2);
21945
+ if (typeof f.id !== "string" || !/^fld[A-Za-z0-9]+$/.test(f.id) || !text2(f.name) || !text2(f.type))
21946
+ fail2("Malformed Airtable field");
21947
+ return { id: f.id, name: f.name, type: f.type, ...f.options === undefined ? {} : { options: serializable(f.options) } };
21948
+ });
21949
+ if (!metadata.some((f) => f.id === t.primaryFieldId) || new Set(metadata.map((f) => f.id)).size !== metadata.length)
21950
+ fail2("Malformed Airtable fields");
21951
+ tables.push({ id: t.id, name: t.name, primaryFieldId: t.primaryFieldId, fields: metadata });
21952
+ }
21953
+ offset = nextOffset(page.offset, seen);
21954
+ } while (offset);
21955
+ return tables;
21936
21956
  }
21937
- return v;
21938
- }
21939
- function validDate(value) {
21940
- const parsed = new Date(`${value}T00:00:00Z`);
21941
- return !value.startsWith("0000-") && Number.isFinite(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
21942
- }
21943
- function validateRecordTableInput(value) {
21944
- const v = object2(value, ["repository", "commit", "columns", "rows", "metadata"], ["columns", "rows"], "record table");
21945
- const captured = v.repository !== undefined;
21946
- if ((captured ? !validRepositoryAlias(v.repository) || !/^[0-9a-f]{40}$/.test(v.commit) : v.commit !== undefined) || !Array.isArray(v.columns) || !v.columns.length || !Array.isArray(v.rows) || !v.rows.length)
21947
- fail2("Invalid record-table capture");
21948
- const ids = new Set;
21949
- for (const entry of v.columns) {
21950
- const c = object2(entry, ["id", "label", "type", "unit", "values", "missing", "missingLabel"], ["id", "label", "type", "missing"], "column");
21951
- if (!text2(c.id) || !text2(c.label) || ids.has(c.id) || c.missing !== "reject" || !["string", "date", "integer", "number", "boolean", "enum", "nullable-enum", "link"].includes(c.type) || (["integer", "number"].includes(c.type) ? !text2(c.unit) : c.unit !== undefined) || (c.type === "nullable-enum" ? !text2(c.missingLabel) : c.missingLabel !== undefined))
21952
- fail2("Invalid column definition or units");
21953
- if (["enum", "nullable-enum"].includes(c.type)) {
21954
- if (!Array.isArray(c.values) || !c.values.length || c.values.some((s) => !text2(s)) || new Set(c.values).size !== c.values.length)
21955
- fail2("Enum columns require distinct values");
21956
- } else if (c.values !== undefined)
21957
- fail2("Only enum columns accept values");
21958
- ids.add(c.id);
21957
+ if (!text2(input.table) || input.table === "." || input.table === "..")
21958
+ fail2("Invalid Airtable table");
21959
+ if (mutation && !tableId(input.table))
21960
+ fail2("Airtable writes require a stable table ID");
21961
+ const path = `${baseId}/${encodeURIComponent(input.table)}`;
21962
+ if (operation === "records.get") {
21963
+ keys2(input, ["table", "id"]);
21964
+ if (!recordId(input.id))
21965
+ fail2("Invalid Airtable record ID");
21966
+ const result2 = record(await request(`${path}/${input.id}`));
21967
+ if (result2.id !== input.id)
21968
+ fail2("Malformed Airtable record");
21969
+ return result2;
21959
21970
  }
21960
- const refs = new Set;
21961
- for (const entry of v.rows) {
21962
- const r = object2(entry, ["ref", "version", "values"], ["ref", "values"], "row");
21963
- if (!text2(r.ref) || !/^[a-zA-Z0-9][a-zA-Z0-9._/-]*$/.test(r.ref) || r.ref.split("/").some((s) => !s || s === "." || s === "..") || refs.has(r.ref) || (captured ? !/^record-v1:[0-9a-f]{64}$/.test(r.version) : r.version !== undefined))
21964
- fail2("Invalid stable record reference or reviewed version");
21965
- const values = object2(r.values, [...ids], [...ids], "projected values");
21966
- for (const c of v.columns) {
21967
- const cell = values[c.id];
21968
- const valid = c.type === "nullable-enum" && cell === null || ["string", "enum", "nullable-enum", "link", "date"].includes(c.type) && typeof cell === "string" && (!c.values || c.values.includes(cell)) && (c.type !== "link" || /^\[\[[a-zA-Z0-9][a-zA-Z0-9._/-]*\]\]$/.test(cell)) && (c.type !== "date" || /^\d{4}-\d{2}-\d{2}$/.test(cell) && validDate(cell)) || ["integer", "number"].includes(c.type) && typeof cell === "number" && Number.isFinite(cell) && (c.type !== "integer" || Number.isSafeInteger(cell)) || c.type === "boolean" && typeof cell === "boolean";
21969
- if (!valid)
21970
- fail2(`Invalid projected value: ${c.id}`);
21971
- }
21972
- refs.add(r.ref);
21971
+ if (operation === "records.list") {
21972
+ keys2(input, ["table", "fields"], ["table"]);
21973
+ if (input.fields !== undefined && (!Array.isArray(input.fields) || !input.fields.length || input.fields.some((f) => !text2(f)) || new Set(input.fields).size !== input.fields.length))
21974
+ fail2("Invalid Airtable field selection");
21975
+ const records2 = [], ids2 = new Set, seen = new Set;
21976
+ let offset;
21977
+ do {
21978
+ const query2 = new URLSearchParams;
21979
+ for (const field of input.fields ?? [])
21980
+ query2.append("fields[]", field);
21981
+ if (offset)
21982
+ query2.set("offset", offset);
21983
+ const page = await request(`${path}?${query2}`);
21984
+ if (!Array.isArray(page.records))
21985
+ fail2("Malformed Airtable records");
21986
+ for (const value of page.records) {
21987
+ const r = record(value);
21988
+ if (ids2.has(r.id))
21989
+ fail2("Malformed Airtable pagination");
21990
+ ids2.add(r.id);
21991
+ records2.push(r);
21992
+ }
21993
+ offset = nextOffset(page.offset, seen);
21994
+ } while (offset);
21995
+ return records2;
21973
21996
  }
21974
- if (v.metadata !== undefined && (!v.metadata || typeof v.metadata !== "object" || Array.isArray(v.metadata)))
21975
- fail2("Metadata must be an object");
21976
- return v;
21997
+ if (!mutation)
21998
+ fail2("Unsupported Airtable operation");
21999
+ keys2(input, ["table", "records"]);
22000
+ if (!Array.isArray(input.records) || !input.records.length || input.records.length > 10)
22001
+ fail2("Invalid Airtable batch");
22002
+ const kind = operation.slice(8), ids = new Set;
22003
+ const records = input.records.map((value) => {
22004
+ const r = object2(value);
22005
+ keys2(r, kind === "create" ? ["fields"] : kind === "update" ? ["id", "expected", "set"] : ["id", "expected"]);
22006
+ if (kind === "create")
22007
+ return { fields: fields2(r.fields, true) };
22008
+ if (!recordId(r.id) || ids.has(r.id))
22009
+ fail2("Invalid Airtable batch record ID");
22010
+ ids.add(r.id);
22011
+ const set = kind === "update" ? fields2(r.set, true) : undefined;
22012
+ if (set && Object.values(set).some((value2) => value2 === null))
22013
+ fail2("Airtable guard null cannot be an update value");
22014
+ return { id: r.id, expected: fields2(r.expected), ...set ? { set } : {} };
22015
+ });
22016
+ for (const r of records) {
22017
+ if (!("id" in r))
22018
+ continue;
22019
+ const current = record(await request(`${path}/${r.id}`));
22020
+ if (current.id !== r.id)
22021
+ fail2("Malformed Airtable record");
22022
+ for (const [key, value] of Object.entries(r.expected)) {
22023
+ if (value === null ? Object.hasOwn(current.fields, key) : !Object.hasOwn(current.fields, key) || !equal(current.fields[key], value))
22024
+ fail2("Airtable reviewed values are stale");
22025
+ }
22026
+ }
22027
+ await options.beforeMutation?.();
22028
+ const query = new URLSearchParams;
22029
+ if (kind === "delete")
22030
+ for (const id2 of ids)
22031
+ query.append("records[]", id2);
22032
+ const result = await request(`${path}${kind === "delete" ? `?${query}` : ""}`, {
22033
+ method: kind === "create" ? "POST" : kind === "update" ? "PATCH" : "DELETE",
22034
+ ...kind === "delete" ? {} : { body: JSON.stringify({ records: records.map((r) => ("id" in r) ? { id: r.id, fields: r.set } : r) }) }
22035
+ }, true);
22036
+ const acknowledged = [], returned = [];
22037
+ try {
22038
+ if (!Array.isArray(result.records))
22039
+ throw new Error;
22040
+ for (const value of result.records) {
22041
+ const r = object2(value);
22042
+ if (!recordId(r.id) || acknowledged.includes(r.id) || kind !== "create" && !ids.has(r.id) || kind === "delete" && r.deleted !== true)
22043
+ throw new Error;
22044
+ acknowledged.push(r.id);
22045
+ if (kind !== "delete")
22046
+ returned.push(record(r));
22047
+ }
22048
+ if (acknowledged.length !== records.length)
22049
+ throw new Error;
22050
+ } catch {
22051
+ throw new AirtableOperationError("Incomplete Airtable mutation acknowledgment", acknowledged.length ? "partial" : "uncertain", acknowledged.length ? [receipt(acknowledged)] : []);
22052
+ }
22053
+ return { receipt: receipt(acknowledged), ...kind === "delete" ? {} : { records: returned } };
21977
22054
  }
21978
- var registeredViews = commonViews;
21979
- function resolveInboxView(view, workspace) {
21980
- const entry = registeredViews.find((entry2) => entry2.name === view.view && (entry2.workspaces === "*" || entry2.workspaces.includes(workspace)));
21981
- if (!entry)
21982
- fail2("View is unknown or unavailable to this workspace");
21983
- return { ...view, input: serializable(entry.validate(view.input)), implementation: entry.implementation };
22055
+ function nextOffset(value, seen) {
22056
+ if (value === undefined)
22057
+ return;
22058
+ if (!text2(value) || seen.has(value))
22059
+ fail2("Malformed Airtable pagination");
22060
+ seen.add(value);
22061
+ return value;
22062
+ }
22063
+
22064
+ // ../../shared/airtable-transport.ts
22065
+ var MANAGED_CONNECTION_CAPABILITY_ENV = "ODDINGTON_MANAGED_CONNECTION_CAPABILITY";
22066
+ function managedConnections(capability) {
22067
+ const endpoint = new URL(capability.endpoint);
22068
+ if (endpoint.protocol !== "http:" || endpoint.hostname !== "127.0.0.1" || !endpoint.port || endpoint.pathname !== "/" || endpoint.search || endpoint.hash || endpoint.username || endpoint.password || !/^[0-9a-f-]{36}$/.test(capability.token))
22069
+ throw new AirtableOperationError("Airtable capability unavailable");
22070
+ const transport = async (name, operation, input) => {
22071
+ let safeInput;
22072
+ try {
22073
+ safeInput = serializable(input);
22074
+ } catch {
22075
+ safeInput = null;
22076
+ }
22077
+ try {
22078
+ const response = await fetch(new URL("airtable", endpoint), {
22079
+ method: "POST",
22080
+ redirect: "error",
22081
+ headers: { authorization: `Bearer ${capability.token}`, "content-type": "application/json" },
22082
+ body: JSON.stringify({ name, operation, input: safeInput }),
22083
+ signal: AbortSignal.timeout(125000)
22084
+ });
22085
+ const value = await response.json();
22086
+ if (!response.ok)
22087
+ throw new AirtableOperationError("Airtable operation failed; the execution retains its evidence", value.effect === "partial" || value.effect === "uncertain" ? value.effect : "unapplied", value.receipts ?? []);
22088
+ return value;
22089
+ } catch (error) {
22090
+ if (error instanceof AirtableOperationError)
22091
+ throw error;
22092
+ throw new AirtableOperationError("Airtable parent acknowledgment unavailable", ["records.create", "records.update", "records.delete"].includes(operation) ? "uncertain" : "unapplied");
22093
+ }
22094
+ };
22095
+ return createConnections(transport);
21984
22096
  }
22097
+
22098
+ // src/connections.ts
22099
+ var connections = createConnections(async (name, operation, input) => {
22100
+ try {
22101
+ const managed = process.env[MANAGED_CONNECTION_CAPABILITY_ENV];
22102
+ if (managed) {
22103
+ const client = managedConnections(JSON.parse(managed)).airtable(name);
22104
+ if (operation === "tables.list")
22105
+ return await client.tables.list();
22106
+ return await client.records[operation.slice(8)](input);
22107
+ }
22108
+ const raw = JSON.parse(process.env[ROUTINE_CONNECTION_CREDENTIALS_ENV] ?? "[]");
22109
+ if (!Array.isArray(raw))
22110
+ throw new Error;
22111
+ const leases = raw.map((value) => parseConnectionLease(value));
22112
+ if (leases.some((lease2) => lease2.provider !== "airtable") || new Set(leases.map((lease2) => lease2.name)).size !== leases.length)
22113
+ throw new Error;
22114
+ const lease = leases.find((lease2) => lease2.name === name);
22115
+ if (!lease || lease.provider !== "airtable")
22116
+ throw new Error;
22117
+ return await executeAirtableOperation(lease, operation, input);
22118
+ } catch (error) {
22119
+ if (error instanceof AirtableOperationError)
22120
+ throw error;
22121
+ throw new AirtableOperationError("Airtable connection unavailable");
22122
+ }
22123
+ });
21985
22124
  // ../../shared/delimited-text.ts
21986
22125
  function delimiterFor(value) {
21987
22126
  const counts = new Map([
@@ -22374,37 +22513,20 @@ function routinePublicationClient(options = {}) {
22374
22513
  };
22375
22514
  }
22376
22515
  export {
22377
- validateResponseDefinition,
22378
- validateResponse,
22379
- validateResolution,
22380
- validateRecordViewInput,
22381
- validateRecordTableInput,
22382
- validateRecordSource,
22383
- validateRecordReference,
22384
- validatePublication,
22385
- validateInboxFile,
22386
- validateHandlerTransition,
22387
- validateCapturedRecordTableInput,
22388
- validateBlock,
22389
- serializable,
22390
22516
  selection,
22391
22517
  routinePublicationClient,
22392
22518
  richTextBlock,
22393
22519
  respond,
22394
- resolveInboxView,
22395
22520
  resolveAsk,
22396
- registeredViews,
22397
22521
  recordsBlock,
22398
22522
  recordBlock,
22399
- recipientPresentation,
22400
22523
  question,
22401
- oddington,
22402
22524
  fileBlock,
22403
22525
  defineHandler,
22404
- createInboxPublisher,
22405
22526
  continueAsk,
22406
- commonViews,
22527
+ connections,
22407
22528
  asksBlock,
22408
22529
  RoutineRecordWriteError,
22409
- InboxContractError
22530
+ InboxContractError,
22531
+ AirtableOperationError
22410
22532
  };
@@ -2,9 +2,16 @@ import type { AskInput, CapturedRecords, DigestInput, InboxFile, PublicationRequ
2
2
  import type { FileAddOptions } from '../../../shared/inbox-files.js';
3
3
  import type { RecordReadInput } from '../../../shared/record-capture.js';
4
4
  import type { RecordDeleteRequest, RecordUpdateRequest, RecordWriteRequest } from '../../../shared/record-write.js';
5
- export * from '../../../shared/inbox-contract.js';
6
- export * from '../../../shared/inbox-views.js';
5
+ export { InboxContractError, asksBlock, continueAsk, defineHandler, fileBlock, question, recordBlock, recordsBlock, resolveAsk, respond, richTextBlock, selection, } from '../../../shared/inbox-contract.js';
6
+ export type { AskInput, AuthoredSource, CapturedRecords, CsvDetails, DigestInput, Handler, HandlerReference, HandlerRuntime, HandlerTransition, InboxBlock, InboxFile, JsonObject, JsonValue, PresentedField, PresentedRecord, PresentedRecordSummary, PublishedAsk, PublishedDigest, PublishedView, PublicationRequest, Question, QuestionInput, QuestionResponse, RecordCreate, RecordDelete, RecordReference, RecordSource, RecordUpdate, RepositoryCapabilities, RepositoryReceipt, Resolution, ResolutionInput, ResponseDefinition, } from '../../../shared/inbox-contract.js';
7
+ export type { CapturedRecordTableInput, RecordField, RecordTableInput, RecordValue, RecordViewInput, TableColumn, ViewRecord, } from '../../../shared/inbox-views.js';
8
+ export type { FileAddOptions } from '../../../shared/inbox-files.js';
9
+ export type { RecordReadInput } from '../../../shared/record-capture.js';
10
+ export type { RecordDeleteRequest, RecordUpdateRequest, RecordWriteRequest } from '../../../shared/record-write.js';
7
11
  export { RoutineRecordWriteError } from '../../../shared/record-write.js';
12
+ export { AirtableOperationError } from '../../../shared/airtable-contract.js';
13
+ export type { AirtableField, AirtableTable, AirtableRecord, AirtableListInput, AirtableGetInput, AirtableCreateInput, AirtableUpdateInput, AirtableDeleteInput, AirtableReceipt, AirtableWriteResult, AirtableClient, Connections } from '../../../shared/airtable-contract.js';
14
+ export declare const connections: import('../../../shared/airtable-contract.js').Connections;
8
15
  export type InboxRequester = (path: string, init?: RequestInit) => Promise<Response>;
9
16
  export type RoutinePublicationClientOptions = {
10
17
  workspace?: string;
@@ -0,0 +1,82 @@
1
+ import type { JsonObject, JsonValue } from './inbox-contract.js';
2
+ export type AirtableField = {
3
+ id: string;
4
+ name: string;
5
+ type: string;
6
+ options?: JsonValue;
7
+ };
8
+ export type AirtableTable = {
9
+ id: string;
10
+ name: string;
11
+ primaryFieldId: string;
12
+ fields: AirtableField[];
13
+ };
14
+ export type AirtableRecord = {
15
+ id: string;
16
+ createdTime?: string;
17
+ fields: JsonObject;
18
+ };
19
+ export type AirtableListInput = {
20
+ table: string;
21
+ fields?: string[];
22
+ };
23
+ export type AirtableGetInput = {
24
+ table: string;
25
+ id: string;
26
+ };
27
+ export type AirtableCreateInput = {
28
+ table: string;
29
+ records: Array<{
30
+ fields: JsonObject;
31
+ }>;
32
+ };
33
+ export type AirtableUpdateInput = {
34
+ table: string;
35
+ records: Array<{
36
+ id: string;
37
+ expected: JsonObject;
38
+ set: JsonObject;
39
+ }>;
40
+ };
41
+ export type AirtableDeleteInput = {
42
+ table: string;
43
+ records: Array<{
44
+ id: string;
45
+ expected: JsonObject;
46
+ }>;
47
+ };
48
+ export type AirtableReceipt = {
49
+ provider: 'airtable';
50
+ connection: `airtable/${string}`;
51
+ baseId: string;
52
+ table: string;
53
+ operation: 'create' | 'update' | 'delete';
54
+ records: Array<{
55
+ id: string;
56
+ }>;
57
+ };
58
+ export type AirtableWriteResult = {
59
+ receipt: AirtableReceipt;
60
+ records?: AirtableRecord[];
61
+ };
62
+ export interface AirtableClient {
63
+ tables: {
64
+ list(): Promise<AirtableTable[]>;
65
+ };
66
+ records: {
67
+ list(input: AirtableListInput): Promise<AirtableRecord[]>;
68
+ get(input: AirtableGetInput): Promise<AirtableRecord>;
69
+ create(input: AirtableCreateInput): Promise<AirtableWriteResult>;
70
+ update(input: AirtableUpdateInput): Promise<AirtableWriteResult>;
71
+ delete(input: AirtableDeleteInput): Promise<AirtableWriteResult>;
72
+ };
73
+ }
74
+ export interface Connections {
75
+ airtable(name: string): AirtableClient;
76
+ }
77
+ /** Safe provider failure. An uncertain or partial mutation must never be retried blindly. */
78
+ export declare class AirtableOperationError extends Error {
79
+ readonly effect: 'unapplied' | 'partial' | 'uncertain';
80
+ readonly receipts: AirtableReceipt[];
81
+ constructor(message: string, effect?: 'unapplied' | 'partial' | 'uncertain', receipts?: AirtableReceipt[]);
82
+ }
@@ -188,17 +188,12 @@ export type QuestionInstance = {
188
188
  export type ResponseSubmission = QuestionInstance & {
189
189
  response: QuestionResponse;
190
190
  };
191
- export type OddingtonAssignment = {
192
- type: 'oddington';
193
- reason: string;
194
- };
195
191
  export type Resolution = {
196
192
  outcome: string;
197
193
  evidence?: JsonValue[];
198
194
  };
199
195
  export type HandlerTransition = {
200
196
  type: 'continue';
201
- assignTo?: OddingtonAssignment;
202
197
  } | ({
203
198
  type: 'resolve';
204
199
  } & Resolution);
@@ -268,6 +263,7 @@ export type HandlerRuntime<Input extends JsonValue = JsonValue> = {
268
263
  responseDefinition: ResponseDefinition;
269
264
  questionId: string;
270
265
  repository: RepositoryCapabilities;
266
+ connections: import('./airtable-contract.js').Connections;
271
267
  };
272
268
  export type Handler<Input extends JsonValue = JsonValue> = {
273
269
  id: string;
@@ -322,12 +318,7 @@ export declare function validatePublication(value: unknown): Publication;
322
318
  /** Structural projection only. The read boundary must check current recipient,
323
319
  * workspace and repository/file access before returning this content. */
324
320
  export declare function recipientPresentation(value: unknown): RecipientPublication;
325
- export declare function oddington(value: {
326
- reason: string;
327
- }): OddingtonAssignment;
328
- export declare function continueAsk(value: {
329
- assignTo?: OddingtonAssignment;
330
- }): HandlerTransition;
321
+ export declare function continueAsk(value: Record<string, never>): HandlerTransition;
331
322
  export declare function resolveAsk(value: Resolution): HandlerTransition;
332
323
  /** Call at the durable transition boundary with server-derived remaining work,
333
324
  * excluding the execution being completed. Lifecycle/effect guards also apply. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oddington/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Oddington routine and response-handler SDK",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",