@hasna/instructions 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +123 -6
  2. package/dist/chunks/{apply-ynfn1av7.js → apply-7svah2qm.js} +5 -3
  3. package/dist/chunks/{apply-sfzja1f3.js → apply-jp13sa1e.js} +6 -3
  4. package/dist/chunks/database-wstmr56z.js +23 -0
  5. package/dist/chunks/{index-85n5wnd8.js → index-1cnyjnd7.js} +5 -7
  6. package/dist/chunks/{index-pkscjnj5.js → index-3djthcm6.js} +0 -1
  7. package/dist/chunks/{index-v7dkb384.js → index-4pfq42t5.js} +129 -77
  8. package/dist/chunks/index-5d7zdftc.js +186 -0
  9. package/dist/chunks/{index-1hpwskwd.js → index-7m4j8a33.js} +205 -329
  10. package/dist/chunks/index-8rb7ng2y.js +148 -0
  11. package/dist/chunks/index-arrrx0ed.js +344 -0
  12. package/dist/chunks/{index-9p9gn7gh.js → index-bzyxvwcd.js} +5 -7
  13. package/dist/chunks/index-fpeyzn3n.js +186 -0
  14. package/dist/chunks/{index-axhwg61p.js → index-fwbs6tma.js} +15 -4
  15. package/dist/chunks/index-n3fjspr0.js +119 -0
  16. package/dist/chunks/{index-e9epbsvs.js → index-z3ma4aqk.js} +15 -4
  17. package/dist/chunks/legacy-store-migration-z2m5yzyk.js +322 -0
  18. package/dist/chunks/{local-a9nen65z.js → local-5wvv5xv3.js} +2 -2
  19. package/dist/chunks/{local-0d8qrc92.js → local-zxa5dpf4.js} +13 -188
  20. package/dist/chunks/s3-backup-n2z4fwyh.js +240 -0
  21. package/dist/chunks/s3-config-hhght91k.js +14 -0
  22. package/dist/chunks/s3-object-store-90tgbh6k.js +14 -0
  23. package/dist/chunks/{sync-m46fc257.js → sync-2075deve.js} +5 -4
  24. package/dist/chunks/{sync-vy1yjvs6.js → sync-r8t7zsp3.js} +4 -4
  25. package/dist/chunks/{template-02wjvv05.js → template-fsfxpe4p.js} +1 -1
  26. package/dist/chunks/{template-ke972qqe.js → template-wgnzdn7h.js} +1 -1
  27. package/dist/cli/index.js +1011 -247
  28. package/dist/data/config-store.d.ts +1 -0
  29. package/dist/data/config-store.d.ts.map +1 -1
  30. package/dist/index.js +840 -215
  31. package/dist/lib/apply.d.ts +7 -0
  32. package/dist/lib/apply.d.ts.map +1 -1
  33. package/dist/lib/compact-output.d.ts +1 -0
  34. package/dist/lib/compact-output.d.ts.map +1 -1
  35. package/dist/lib/export.d.ts +22 -3
  36. package/dist/lib/export.d.ts.map +1 -1
  37. package/dist/lib/import.d.ts +18 -0
  38. package/dist/lib/import.d.ts.map +1 -1
  39. package/dist/lib/legacy-store-migration.d.ts +39 -0
  40. package/dist/lib/legacy-store-migration.d.ts.map +1 -0
  41. package/dist/lib/managed-skill-runtimes.d.ts +1 -0
  42. package/dist/lib/managed-skill-runtimes.d.ts.map +1 -1
  43. package/dist/lib/sync-dir.d.ts.map +1 -1
  44. package/dist/mcp/index.js +31 -20
  45. package/dist/sdk/index.d.ts +2 -0
  46. package/dist/sdk/index.d.ts.map +1 -1
  47. package/dist/sdk/index.js +205 -10
  48. package/dist/sdk/resolve.d.ts +1 -1
  49. package/dist/sdk/resolve.d.ts.map +1 -1
  50. package/dist/sdk/v1-client.d.ts +57 -0
  51. package/dist/sdk/v1-client.d.ts.map +1 -0
  52. package/dist/sdk/v1.generated.d.ts +212 -21
  53. package/dist/sdk/v1.generated.d.ts.map +1 -1
  54. package/dist/server/cloud.d.ts.map +1 -1
  55. package/dist/server/index.d.ts +13 -5
  56. package/dist/server/index.d.ts.map +1 -1
  57. package/dist/server/index.js +1180 -432
  58. package/dist/server/openapi.d.ts +1264 -92
  59. package/dist/server/openapi.d.ts.map +1 -1
  60. package/dist/server/v1.d.ts +25 -1
  61. package/dist/server/v1.d.ts.map +1 -1
  62. package/dist/storage/cloud-store.d.ts +57 -1
  63. package/dist/storage/cloud-store.d.ts.map +1 -1
  64. package/dist/storage/index.d.ts +7 -0
  65. package/dist/storage/index.d.ts.map +1 -0
  66. package/dist/storage/index.js +725 -0
  67. package/dist/storage/s3-backup.d.ts +76 -0
  68. package/dist/storage/s3-backup.d.ts.map +1 -0
  69. package/dist/storage/s3-config.d.ts +46 -0
  70. package/dist/storage/s3-config.d.ts.map +1 -0
  71. package/dist/storage/s3-object-store.d.ts +70 -0
  72. package/dist/storage/s3-object-store.d.ts.map +1 -0
  73. package/dist/types/index.d.ts +65 -3
  74. package/dist/types/index.d.ts.map +1 -1
  75. package/hasna.contract.json +110 -0
  76. package/migrations/0001_instructions.sql +63 -0
  77. package/migrations/0002_api_keys.sql +20 -0
  78. package/migrations/0003_idempotency_receipts.sql +24 -0
  79. package/package.json +22 -11
  80. package/assets/skills/inbox/SKILL.md +0 -86
@@ -70,9 +70,9 @@ function parseCursor(value) {
70
70
  const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
71
71
  if (!Number.isFinite(parsed) || parsed < 0)
72
72
  return 0;
73
- return Math.floor(parsed);
73
+ return Math.min(Math.floor(parsed), MAX_LIST_CURSOR);
74
74
  }
75
- var DEFAULT_LIST_LIMIT = 20, MAX_LIST_LIMIT = 100;
75
+ var DEFAULT_LIST_LIMIT = 20, MAX_LIST_LIMIT = 100, MAX_LIST_CURSOR = 1e5;
76
76
 
77
77
  // src/lib/bounded-read.ts
78
78
  function normalizeBoundedReadOptions(options = {}) {
@@ -105,7 +105,7 @@ function boundedReadPage(items, total, options = {}) {
105
105
  }
106
106
  var init_bounded_read = () => {};
107
107
 
108
- // ../contracts/dist/client/transport.js
108
+ // ../../node_modules/.bun/@hasna+contracts@1.0.2+e8014c875821e0be/node_modules/@hasna/contracts/dist/client/transport.js
109
109
  import { createRequire } from "module";
110
110
  var MAX_CREDENTIAL_FILE_BYTES, INSPECT_CUSTOM, CREDENTIAL_SEAL, AMBIENT_ENVIRONMENT, SECRETS_PACKAGE_SPECIFIER, requireSecretsSdk, IDEMPOTENT_METHODS, AUTHORITY_OVERRIDE_HEADERS;
111
111
  var init_transport = __esm(() => {
@@ -125,7 +125,7 @@ var init_transport = __esm(() => {
125
125
  ]);
126
126
  });
127
127
 
128
- // ../contracts/dist/client/storage.js
128
+ // ../../node_modules/.bun/@hasna+contracts@1.0.2+e8014c875821e0be/node_modules/@hasna/contracts/dist/client/storage.js
129
129
  import { createRequire as createRequire2 } from "module";
130
130
  var MAX_CREDENTIAL_FILE_BYTES2, INSPECT_CUSTOM2, CREDENTIAL_SEAL2, AMBIENT_ENVIRONMENT2, SECRETS_PACKAGE_SPECIFIER2, requireSecretsSdk2, IDEMPOTENT_METHODS2, AUTHORITY_OVERRIDE_HEADERS2;
131
131
  var init_storage = __esm(() => {
@@ -5156,7 +5156,7 @@ var init_machine = __esm(() => {
5156
5156
  init_template();
5157
5157
  });
5158
5158
 
5159
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/compose.js
5159
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/compose.js
5160
5160
  var compose = (middleware, onError, onNotFound) => {
5161
5161
  return (context, next) => {
5162
5162
  let index = -1;
@@ -5200,10 +5200,10 @@ var compose = (middleware, onError, onNotFound) => {
5200
5200
  };
5201
5201
  };
5202
5202
 
5203
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/request/constants.js
5203
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/request/constants.js
5204
5204
  var GET_MATCH_RESULT = /* @__PURE__ */ Symbol();
5205
5205
 
5206
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/utils/buffer.js
5206
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/utils/buffer.js
5207
5207
  var bufferToFormData = (arrayBuffer, contentType) => {
5208
5208
  const response = new Response(arrayBuffer, {
5209
5209
  headers: {
@@ -5213,7 +5213,9 @@ var bufferToFormData = (arrayBuffer, contentType) => {
5213
5213
  return response.formData();
5214
5214
  };
5215
5215
 
5216
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/utils/body.js
5216
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/utils/body.js
5217
+ var MAX_NESTING_DEPTH = 32;
5218
+ var MAX_NESTED_OBJECTS = 1e4;
5217
5219
  var isRawRequest = (request) => ("headers" in request);
5218
5220
  var parseBody = async (request, options = /* @__PURE__ */ Object.create(null)) => {
5219
5221
  const { all = false, dot = false } = options;
@@ -5243,6 +5245,7 @@ async function parseFormData(request, options) {
5243
5245
  }
5244
5246
  function convertFormDataToBodyData(formData, options) {
5245
5247
  const form = /* @__PURE__ */ Object.create(null);
5248
+ const nestingState = { count: 0 };
5246
5249
  formData.forEach((value, key) => {
5247
5250
  const shouldParseAllValues = options.all || key.endsWith("[]");
5248
5251
  if (!shouldParseAllValues) {
@@ -5255,7 +5258,7 @@ function convertFormDataToBodyData(formData, options) {
5255
5258
  Object.entries(form).forEach(([key, value]) => {
5256
5259
  const shouldParseDotValues = key.includes(".");
5257
5260
  if (shouldParseDotValues) {
5258
- handleParsingNestedValues(form, key, value);
5261
+ handleParsingNestedValues(form, key, value, nestingState);
5259
5262
  delete form[key];
5260
5263
  }
5261
5264
  });
@@ -5277,25 +5280,34 @@ var handleParsingAllValues = (form, key, value) => {
5277
5280
  }
5278
5281
  }
5279
5282
  };
5280
- var handleParsingNestedValues = (form, key, value) => {
5283
+ var handleParsingNestedValues = (form, key, value, state) => {
5281
5284
  if (/(?:^|\.)__proto__\./.test(key)) {
5282
5285
  return;
5283
5286
  }
5284
5287
  let nestedForm = form;
5285
- const keys = key.split(".");
5288
+ const keys = key.split(".", MAX_NESTING_DEPTH + 2);
5289
+ if (keys.length > MAX_NESTING_DEPTH + 1) {
5290
+ throwNestingLimitExceeded();
5291
+ }
5286
5292
  keys.forEach((key2, index) => {
5287
5293
  if (index === keys.length - 1) {
5288
5294
  nestedForm[key2] = value;
5289
5295
  } else {
5290
5296
  if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
5297
+ if (state.count++ >= MAX_NESTED_OBJECTS) {
5298
+ throwNestingLimitExceeded();
5299
+ }
5291
5300
  nestedForm[key2] = /* @__PURE__ */ Object.create(null);
5292
5301
  }
5293
5302
  nestedForm = nestedForm[key2];
5294
5303
  }
5295
5304
  });
5296
5305
  };
5306
+ var throwNestingLimitExceeded = () => {
5307
+ throw new Error("Nesting limit exceeded");
5308
+ };
5297
5309
 
5298
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/utils/url.js
5310
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/utils/url.js
5299
5311
  var splitPath = (path) => {
5300
5312
  const paths = path.split("/");
5301
5313
  if (paths[0] === "") {
@@ -5425,6 +5437,10 @@ var _decodeURI = (value) => {
5425
5437
  return tryDecodeURIComponent(value);
5426
5438
  };
5427
5439
  var _getQueryParam = (url, key, multiple) => {
5440
+ const hashIndex = url.indexOf("#", 8);
5441
+ if (hashIndex !== -1) {
5442
+ url = url.slice(0, hashIndex);
5443
+ }
5428
5444
  let encoded;
5429
5445
  if (!multiple && key && key.indexOf("%") === -1 && key.indexOf("+") === -1) {
5430
5446
  let keyIndex2 = url.indexOf("?", 8);
@@ -5493,7 +5509,7 @@ var getQueryParams = (url, key) => {
5493
5509
  };
5494
5510
  var decodeURIComponent_ = decodeURIComponent;
5495
5511
 
5496
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/request.js
5512
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/request.js
5497
5513
  var HonoRequest = class {
5498
5514
  raw;
5499
5515
  #validatedData;
@@ -5510,13 +5526,13 @@ var HonoRequest = class {
5510
5526
  return key ? this.#getDecodedParam(key) : this.#getAllDecodedParams();
5511
5527
  }
5512
5528
  #getDecodedParam(key) {
5513
- const paramKey = this.#matchResult[0][this.routeIndex][1][key];
5529
+ const paramKey = this.#matchResult[0][this.routeIndex]?.[1][key];
5514
5530
  const param = this.#getParamValue(paramKey);
5515
5531
  return param && tryDecodeURIComponent(param);
5516
5532
  }
5517
5533
  #getAllDecodedParams() {
5518
5534
  const decoded = {};
5519
- const keys = Object.keys(this.#matchResult[0][this.routeIndex][1]);
5535
+ const keys = Object.keys(this.#matchResult[0][this.routeIndex]?.[1] ?? {});
5520
5536
  for (const key of keys) {
5521
5537
  const value = this.#getParamValue(this.#matchResult[0][this.routeIndex][1][key]);
5522
5538
  if (value !== undefined) {
@@ -5604,7 +5620,7 @@ var HonoRequest = class {
5604
5620
  }
5605
5621
  };
5606
5622
 
5607
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/utils/html.js
5623
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/utils/html.js
5608
5624
  var HtmlEscapedCallbackPhase = {
5609
5625
  Stringify: 1,
5610
5626
  BeforeStream: 2,
@@ -5642,7 +5658,7 @@ var resolveCallback = async (str, phase, preserveCallbacks, context, buffer) =>
5642
5658
  }
5643
5659
  };
5644
5660
 
5645
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/context.js
5661
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/context.js
5646
5662
  var TEXT_PLAIN = "text/plain; charset=UTF-8";
5647
5663
  var setDefaultContentType = (contentType, headers) => {
5648
5664
  return {
@@ -5824,7 +5840,7 @@ var Context = class {
5824
5840
  };
5825
5841
  };
5826
5842
 
5827
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/router.js
5843
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/router.js
5828
5844
  var METHOD_NAME_ALL = "ALL";
5829
5845
  var METHOD_NAME_ALL_LOWERCASE = "all";
5830
5846
  var METHODS = ["get", "post", "put", "delete", "options", "patch", "query"];
@@ -5832,10 +5848,10 @@ var MESSAGE_MATCHER_IS_ALREADY_BUILT = "Can not add a route since the matcher is
5832
5848
  var UnsupportedPathError = class extends Error {
5833
5849
  };
5834
5850
 
5835
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/utils/constants.js
5851
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/utils/constants.js
5836
5852
  var COMPOSED_HANDLER = "__COMPOSED_HANDLER";
5837
5853
 
5838
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/hono-base.js
5854
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/hono-base.js
5839
5855
  var notFoundHandler = (c) => {
5840
5856
  return c.text("404 Not Found", 404);
5841
5857
  };
@@ -5867,13 +5883,14 @@ var Hono = class _Hono {
5867
5883
  const allMethods = [...METHODS, METHOD_NAME_ALL_LOWERCASE];
5868
5884
  allMethods.forEach((method) => {
5869
5885
  this[method] = (args1, ...args) => {
5886
+ const methodName = method.toUpperCase();
5870
5887
  if (typeof args1 === "string") {
5871
5888
  this.#path = args1;
5872
5889
  } else {
5873
- this.#addRoute(method, this.#path, args1);
5890
+ this.#addRoute(methodName, this.#path, args1);
5874
5891
  }
5875
5892
  args.forEach((handler) => {
5876
- this.#addRoute(method, this.#path, handler);
5893
+ this.#addRoute(methodName, this.#path, handler);
5877
5894
  });
5878
5895
  return this;
5879
5896
  };
@@ -5882,9 +5899,10 @@ var Hono = class _Hono {
5882
5899
  for (const p of [path].flat()) {
5883
5900
  this.#path = p;
5884
5901
  for (const m of [method].flat()) {
5885
- handlers.map((handler) => {
5886
- this.#addRoute(m.toUpperCase(), this.#path, handler);
5887
- });
5902
+ const methodName = m.toUpperCase();
5903
+ for (const handler of handlers) {
5904
+ this.#addRoute(methodName, this.#path, handler);
5905
+ }
5888
5906
  }
5889
5907
  }
5890
5908
  return this;
@@ -5989,7 +6007,6 @@ var Hono = class _Hono {
5989
6007
  return this;
5990
6008
  }
5991
6009
  #addRoute(method, path, handler, baseRoutePath) {
5992
- method = method.toUpperCase();
5993
6010
  path = mergePath(this._basePath, path);
5994
6011
  const r = {
5995
6012
  basePath: baseRoutePath !== undefined ? mergePath(this._basePath, baseRoutePath) : this._basePath,
@@ -6060,7 +6077,10 @@ var Hono = class _Hono {
6060
6077
  };
6061
6078
  };
6062
6079
 
6063
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/router/reg-exp-router/matcher.js
6080
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/router/utils.js
6081
+ var createNullObject = () => /* @__PURE__ */ Object.create(null);
6082
+
6083
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/matcher.js
6064
6084
  var emptyParam = [];
6065
6085
  function match(method, path) {
6066
6086
  const matchers = this.buildAllMatchers();
@@ -6081,7 +6101,7 @@ function match(method, path) {
6081
6101
  return match2(method, path);
6082
6102
  }
6083
6103
 
6084
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/router/reg-exp-router/node.js
6104
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/node.js
6085
6105
  var LABEL_REG_EXP_STR = "[^/]+";
6086
6106
  var ONLY_WILDCARD_REG_EXP_STR = ".*";
6087
6107
  var TAIL_WILDCARD_REG_EXP_STR = "(?:|/.*)";
@@ -6109,7 +6129,7 @@ function compareKey(a, b) {
6109
6129
  var Node = class _Node {
6110
6130
  #index;
6111
6131
  #varIndex;
6112
- #children = /* @__PURE__ */ Object.create(null);
6132
+ #children = createNullObject();
6113
6133
  insert(tokens, index, paramMap, context, isStatic) {
6114
6134
  let node = this;
6115
6135
  for (let i = 0, len = tokens.length;i < len; i++) {
@@ -6184,12 +6204,12 @@ var Node = class _Node {
6184
6204
  }
6185
6205
  };
6186
6206
 
6187
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/router/reg-exp-router/trie.js
6207
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/trie.js
6188
6208
  var Trie = class {
6189
6209
  #context = { varIndex: 0 };
6190
6210
  #root = new Node;
6191
6211
  #index = 0;
6192
- paths = /* @__PURE__ */ Object.create(null);
6212
+ paths = createNullObject();
6193
6213
  insert(path, isStatic) {
6194
6214
  if (isStatic) {
6195
6215
  this.#root.insert(path.split(""), 0, [], this.#context, true);
@@ -6247,18 +6267,12 @@ var Trie = class {
6247
6267
  }
6248
6268
  };
6249
6269
 
6250
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/router/reg-exp-router/router.js
6251
- var wildcardRegExpCache = /* @__PURE__ */ Object.create(null);
6270
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/router.js
6271
+ var wildcardRegExpCache = createNullObject();
6252
6272
  function buildWildcardRegExp(path) {
6253
- return wildcardRegExpCache[path] ??= new RegExp(path === "*" ? "" : `^${path.replace(/\/\*$|([.\\+*[^\]$()])/g, (_, metaChar) => metaChar ? `\\${metaChar}` : "(?:|/.*)")}$`);
6254
- }
6255
- function clearWildcardRegExpCache() {
6256
- wildcardRegExpCache = /* @__PURE__ */ Object.create(null);
6273
+ return wildcardRegExpCache[path] ??= new RegExp(`^${path.replace(/\/:[^/{}]+(?:\{\[\^\/]\+})?(?=[/{]|$)|\/?\*$|([.\\+*[^\]$()?{}|])/g, (match2, metaChar) => metaChar ? `\\${metaChar}` : match2 === "/*" ? TAIL_WILDCARD_REG_EXP_STR : match2 === "*" ? ONLY_WILDCARD_REG_EXP_STR : `/:${LABEL_REG_EXP_STR}`)}$`);
6257
6274
  }
6258
6275
  function findMiddleware(middleware, path) {
6259
- if (!middleware) {
6260
- return;
6261
- }
6262
6276
  for (const k of Object.keys(middleware).sort((a, b) => b.length - a.length)) {
6263
6277
  if (buildWildcardRegExp(k).test(path)) {
6264
6278
  return [...middleware[k]];
@@ -6272,8 +6286,8 @@ var RegExpRouter = class {
6272
6286
  #routes;
6273
6287
  #tries;
6274
6288
  constructor() {
6275
- this.#middleware = { [METHOD_NAME_ALL]: /* @__PURE__ */ Object.create(null) };
6276
- this.#routes = { [METHOD_NAME_ALL]: /* @__PURE__ */ Object.create(null) };
6289
+ this.#middleware = { [METHOD_NAME_ALL]: createNullObject() };
6290
+ this.#routes = { [METHOD_NAME_ALL]: createNullObject() };
6277
6291
  this.#tries = { [METHOD_NAME_ALL]: new Trie };
6278
6292
  }
6279
6293
  #insertPath(method, path) {
@@ -6286,119 +6300,90 @@ var RegExpRouter = class {
6286
6300
  add(method, path, handler) {
6287
6301
  const middleware = this.#middleware;
6288
6302
  const routes = this.#routes;
6289
- if (!middleware || !routes) {
6303
+ if (!middleware) {
6290
6304
  throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT);
6291
6305
  }
6292
6306
  if (!middleware[method]) {
6293
6307
  this.#tries[method] = new Trie;
6294
- [middleware, routes].forEach((handlerMap) => {
6295
- handlerMap[method] = /* @__PURE__ */ Object.create(null);
6296
- Object.keys(handlerMap[METHOD_NAME_ALL]).forEach((p) => {
6308
+ for (const handlerMap of [middleware, routes]) {
6309
+ handlerMap[method] = createNullObject();
6310
+ for (const p in handlerMap[METHOD_NAME_ALL]) {
6297
6311
  handlerMap[method][p] = [...handlerMap[METHOD_NAME_ALL][p]];
6298
6312
  this.#insertPath(method, p);
6299
- });
6300
- });
6313
+ }
6314
+ }
6301
6315
  }
6302
6316
  if (path === "/*") {
6303
6317
  path = "*";
6304
6318
  }
6305
- const paramCount = (path.match(/\/:/g) || []).length;
6319
+ const methods = method === METHOD_NAME_ALL ? Object.keys(middleware) : [method];
6306
6320
  if (/\*$/.test(path)) {
6307
6321
  const re = buildWildcardRegExp(path);
6308
- Object.keys(middleware).forEach((m) => {
6309
- if ((method === METHOD_NAME_ALL || method === m) && !middleware[m][path]) {
6322
+ for (const m of methods) {
6323
+ if (!middleware[m][path]) {
6310
6324
  this.#insertPath(m, path);
6311
6325
  middleware[m][path] = findMiddleware(middleware[m], path) || findMiddleware(middleware[METHOD_NAME_ALL], path) || [];
6312
6326
  }
6313
- });
6314
- Object.keys(middleware).forEach((m) => {
6315
- if (method === METHOD_NAME_ALL || method === m) {
6316
- Object.keys(middleware[m]).forEach((p) => {
6317
- re.test(p) && middleware[m][p].push([handler, paramCount]);
6318
- });
6319
- }
6320
- });
6321
- Object.keys(routes).forEach((m) => {
6322
- if (method === METHOD_NAME_ALL || method === m) {
6323
- Object.keys(routes[m]).forEach((p) => re.test(p) && routes[m][p].push([handler, paramCount]));
6327
+ }
6328
+ for (const handlerMap of [middleware, routes]) {
6329
+ for (const m of methods) {
6330
+ for (const p in handlerMap[m]) {
6331
+ re.test(p) && handlerMap[m][p].push([handler, path]);
6332
+ }
6324
6333
  }
6325
- });
6334
+ }
6326
6335
  return;
6327
6336
  }
6328
6337
  const paths = checkOptionalParameter(path) || [path];
6329
- for (let i = 0, len = paths.length;i < len; i++) {
6330
- const path2 = paths[i];
6331
- Object.keys(routes).forEach((m) => {
6332
- if (method === METHOD_NAME_ALL || method === m) {
6333
- if (!routes[m][path2]) {
6334
- this.#insertPath(m, path2);
6335
- routes[m][path2] = [
6336
- ...findMiddleware(middleware[m], path2) || findMiddleware(middleware[METHOD_NAME_ALL], path2) || []
6337
- ];
6338
- }
6339
- routes[m][path2].push([handler, paramCount - len + i + 1]);
6338
+ for (const path2 of paths) {
6339
+ for (const m of methods) {
6340
+ if (!routes[m][path2]) {
6341
+ this.#insertPath(m, path2);
6342
+ routes[m][path2] = findMiddleware(middleware[m], path2) || findMiddleware(middleware[METHOD_NAME_ALL], path2) || [];
6340
6343
  }
6341
- });
6344
+ routes[m][path2].push([handler, path2]);
6345
+ }
6342
6346
  }
6343
6347
  }
6344
6348
  match = match;
6345
6349
  buildAllMatchers() {
6346
- const matchers = /* @__PURE__ */ Object.create(null);
6347
- Object.keys(this.#routes).concat(Object.keys(this.#middleware)).forEach((method) => {
6348
- matchers[method] ||= this.#buildMatcher(method);
6349
- });
6350
+ const matchers = createNullObject();
6351
+ for (const method of Object.keys(this.#routes)) {
6352
+ matchers[method] = this.#buildMatcher(method);
6353
+ }
6350
6354
  this.#middleware = this.#routes = this.#tries = undefined;
6351
- clearWildcardRegExpCache();
6355
+ wildcardRegExpCache = createNullObject();
6352
6356
  return matchers;
6353
6357
  }
6354
6358
  #buildMatcher(method) {
6355
6359
  const middleware = this.#middleware[method];
6356
6360
  const routes = this.#routes[method];
6357
6361
  const trie = this.#tries[method];
6358
- const staticMap = /* @__PURE__ */ Object.create(null);
6362
+ const staticMap = createNullObject();
6359
6363
  const handlerData = [];
6360
- [middleware, routes].forEach((r) => {
6364
+ const [regexp, indexReplacementMap, paramReplacementMap] = trie.buildRegExp();
6365
+ for (const r of [middleware, routes]) {
6361
6366
  for (const path in r) {
6362
6367
  const handlers = r[path];
6363
6368
  const pathData = trie.paths[path];
6364
6369
  if (!pathData) {
6365
- staticMap[path] = [handlers.map(([h]) => [h, /* @__PURE__ */ Object.create(null)]), emptyParam];
6366
- continue;
6367
- }
6368
- const paramAssoc = pathData[1];
6369
- handlerData[pathData[0]] = handlers.map(([h, paramCount]) => {
6370
- const paramIndexMap = /* @__PURE__ */ Object.create(null);
6371
- paramCount -= 1;
6372
- for (;paramCount >= 0; paramCount--) {
6373
- const [key, value] = paramAssoc[paramCount];
6374
- paramIndexMap[key] = value;
6375
- }
6376
- return [h, paramIndexMap];
6377
- });
6378
- }
6379
- });
6380
- const [regexp, indexReplacementMap, paramReplacementMap] = trie.buildRegExp();
6381
- for (let i = 0, len = handlerData.length;i < len; i++) {
6382
- for (let j = 0, len2 = handlerData[i].length;j < len2; j++) {
6383
- const map = handlerData[i][j]?.[1];
6384
- if (!map) {
6370
+ staticMap[path] = [handlers.map(([h]) => [h, createNullObject()]), emptyParam];
6385
6371
  continue;
6386
6372
  }
6387
- const keys = Object.keys(map);
6388
- for (let k = 0, len3 = keys.length;k < len3; k++) {
6389
- map[keys[k]] = paramReplacementMap[map[keys[k]]];
6390
- }
6373
+ handlerData[pathData[0]] = handlers.map(([h, handlerPath]) => [
6374
+ h,
6375
+ trie.paths[handlerPath][1].reduceRight((map, [key], i) => {
6376
+ map[key] = paramReplacementMap[pathData[1][i][1]];
6377
+ return map;
6378
+ }, createNullObject())
6379
+ ]);
6391
6380
  }
6392
6381
  }
6393
- const handlerMap = [];
6394
- for (const i in indexReplacementMap) {
6395
- handlerMap[i] = handlerData[indexReplacementMap[i]];
6396
- }
6397
- return [regexp, handlerMap, staticMap];
6382
+ return [regexp, indexReplacementMap.map((i) => handlerData[i]), staticMap];
6398
6383
  }
6399
6384
  };
6400
6385
 
6401
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/router/reg-exp-router/prepared-router.js
6386
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/prepared-router.js
6402
6387
  var PreparedRegExpRouter = class {
6403
6388
  name = "PreparedRegExpRouter";
6404
6389
  #matchers;
@@ -6470,7 +6455,7 @@ var PreparedRegExpRouter = class {
6470
6455
  match = match;
6471
6456
  };
6472
6457
 
6473
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/router/smart-router/router.js
6458
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/router/smart-router/router.js
6474
6459
  var SmartRouter = class {
6475
6460
  name = "SmartRouter";
6476
6461
  #routers = [];
@@ -6525,12 +6510,12 @@ var SmartRouter = class {
6525
6510
  }
6526
6511
  };
6527
6512
 
6528
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/router/trie-router/node.js
6529
- var emptyParams = /* @__PURE__ */ Object.create(null);
6513
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/router/trie-router/node.js
6514
+ var emptyParams = createNullObject();
6530
6515
  var order = 0;
6531
6516
  var Node2 = class _Node2 {
6532
6517
  #methods = [];
6533
- #children = /* @__PURE__ */ Object.create(null);
6518
+ #children = createNullObject();
6534
6519
  #patterns = [];
6535
6520
  #pattern;
6536
6521
  #params = emptyParams;
@@ -6567,7 +6552,7 @@ var Node2 = class _Node2 {
6567
6552
  const m = node.#methods[i];
6568
6553
  const handlerSet = m[method] || m[METHOD_NAME_ALL];
6569
6554
  if (handlerSet) {
6570
- handlerSet.params = /* @__PURE__ */ Object.create(null);
6555
+ handlerSet.params = createNullObject();
6571
6556
  handlerSets.push(handlerSet);
6572
6557
  for (let i2 = 0, len2 = handlerSet.possibleKeys.length;i2 < len2; i2++) {
6573
6558
  const key = handlerSet.possibleKeys[i2];
@@ -6673,7 +6658,7 @@ var Node2 = class _Node2 {
6673
6658
  }
6674
6659
  };
6675
6660
 
6676
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/router/trie-router/router.js
6661
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/router/trie-router/router.js
6677
6662
  var TrieRouter = class {
6678
6663
  name = "TrieRouter";
6679
6664
  #node = new Node2;
@@ -6687,7 +6672,7 @@ var TrieRouter = class {
6687
6672
  }
6688
6673
  };
6689
6674
 
6690
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/hono.js
6675
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/hono.js
6691
6676
  var Hono2 = class extends Hono {
6692
6677
  constructor(options = {}) {
6693
6678
  super(options);
@@ -6697,7 +6682,7 @@ var Hono2 = class extends Hono {
6697
6682
  }
6698
6683
  };
6699
6684
 
6700
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/middleware/cors/index.js
6685
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/middleware/cors/index.js
6701
6686
  var cors = (options) => {
6702
6687
  const opts = {
6703
6688
  origin: "*",
@@ -6814,7 +6799,7 @@ function getPackageVersion() {
6814
6799
  // src/server/v1.ts
6815
6800
  init_types();
6816
6801
 
6817
- // ../contracts/dist/auth/index.js
6802
+ // ../../node_modules/.bun/@hasna+contracts@1.0.2+e8014c875821e0be/node_modules/@hasna/contracts/dist/auth/index.js
6818
6803
  import { createHash, createHmac, randomBytes, timingSafeEqual } from "crypto";
6819
6804
  var MAX_TENANT_ID_LENGTH = 64;
6820
6805
  var TENANT_ID_PATTERN = new RegExp(`^[A-Za-z0-9][A-Za-z0-9._-]{0,${MAX_TENANT_ID_LENGTH - 1}}$`);
@@ -7769,119 +7754,178 @@ function instructionsSchemaSql() {
7769
7754
  ];
7770
7755
  }
7771
7756
 
7772
- // src/server/cloud.ts
7773
- var INSTRUCTIONS_APP_SLUG = "instructions";
7774
- function resolveCloudDatabaseUrl(env = process.env) {
7775
- return env.HASNA_INSTRUCTIONS_DATABASE_URL || env.INSTRUCTIONS_DATABASE_URL || env.DATABASE_URL || undefined;
7776
- }
7777
- function resolveSigningSecret(env = process.env) {
7778
- return env.HASNA_INSTRUCTIONS_API_SIGNING_KEY?.trim() || env.HASNA_API_SIGNING_KEY?.trim() || env.API_KEY_SIGNING_SECRET?.trim() || undefined;
7779
- }
7780
- function isPostgresBackendEnabled(env = process.env) {
7781
- return Boolean(resolveCloudDatabaseUrl(env));
7782
- }
7783
- var cachedClient = null;
7784
- var cachedStore = null;
7785
- var cachedVerifier = null;
7786
- var schemaEnsured = null;
7787
- function getClient() {
7788
- if (cachedClient)
7789
- return cachedClient;
7790
- const url = resolveCloudDatabaseUrl();
7791
- if (!url) {
7792
- throw new Error("Cloud /v1 requires a remote database URL (HASNA_INSTRUCTIONS_DATABASE_URL / INSTRUCTIONS_DATABASE_URL / DATABASE_URL).");
7757
+ // src/storage/cloud-store.ts
7758
+ init_types();
7759
+ init_bounded_read();
7760
+ init_instruction_graph();
7761
+ init_asset_plan();
7762
+ init_machine();
7763
+ import { createHash as createHash2, randomUUID } from "crypto";
7764
+
7765
+ class CollectionChangedWhilePagingError extends Error {
7766
+ constructor(label, detail) {
7767
+ super(`${label} changed while paging: ${detail}`);
7768
+ this.name = "CollectionChangedWhilePagingError";
7793
7769
  }
7794
- const pool2 = createPgPool({
7795
- connectionString: url,
7796
- max: 6,
7797
- idleTimeoutMillis: 30000,
7798
- connectionTimeoutMillis: 15000,
7799
- applicationName: "instructions-serve"
7800
- });
7801
- cachedClient = createQueryClient(pool2);
7802
- return cachedClient;
7803
7770
  }
7804
- function getCloudClient() {
7805
- return getClient();
7771
+ function isBoundedPageConsistencyError(error) {
7772
+ return error instanceof Error && (error.message.startsWith("bounded read returned ") || error.message.startsWith("bounded read did not advance "));
7806
7773
  }
7807
- function authClient() {
7808
- const client = getClient();
7809
- return {
7810
- async many(sql, params = []) {
7811
- return client.many(sql, params);
7812
- },
7813
- async get(sql, params = []) {
7814
- return client.get(sql, params);
7815
- },
7816
- async execute(sql, params = []) {
7817
- await client.execute(sql, params);
7774
+ async function aggregateBoundedCollectionRead(label, readPage, identity, requireAscendingIdentity = false, retryBoundedPageConsistencyErrors = false) {
7775
+ let lastError = null;
7776
+ for (let attempt = 0;attempt < 2; attempt += 1) {
7777
+ try {
7778
+ const items = [];
7779
+ const seen = new Set;
7780
+ let expectedTotal = null;
7781
+ let previousIdentity = null;
7782
+ let cursor = 0;
7783
+ while (true) {
7784
+ let page;
7785
+ try {
7786
+ page = await readPage(cursor);
7787
+ } catch (error) {
7788
+ if (!retryBoundedPageConsistencyErrors || !isBoundedPageConsistencyError(error))
7789
+ throw error;
7790
+ throw new CollectionChangedWhilePagingError(label, error.message);
7791
+ }
7792
+ if (page.cursor !== cursor) {
7793
+ throw new CollectionChangedWhilePagingError(label, `server returned cursor ${page.cursor} for requested cursor ${cursor}`);
7794
+ }
7795
+ if (expectedTotal === null)
7796
+ expectedTotal = page.total;
7797
+ else if (page.total !== expectedTotal) {
7798
+ throw new CollectionChangedWhilePagingError(label, `total changed from ${expectedTotal} to ${page.total}`);
7799
+ }
7800
+ for (const item of page.items) {
7801
+ const key = identity(item);
7802
+ if (!key)
7803
+ throw new CollectionChangedWhilePagingError(label, "an item had no stable identity");
7804
+ if (seen.has(key))
7805
+ throw new CollectionChangedWhilePagingError(label, `duplicate identity ${key}`);
7806
+ if (requireAscendingIdentity && previousIdentity !== null && key <= previousIdentity) {
7807
+ throw new CollectionChangedWhilePagingError(label, `identity order was not strictly increasing at ${key}`);
7808
+ }
7809
+ seen.add(key);
7810
+ previousIdentity = key;
7811
+ items.push(item);
7812
+ }
7813
+ if (page.complete) {
7814
+ if (items.length !== expectedTotal) {
7815
+ throw new CollectionChangedWhilePagingError(label, `received ${items.length} unique rows for total ${expectedTotal}`);
7816
+ }
7817
+ return { items, total: expectedTotal };
7818
+ }
7819
+ if (page.next_cursor === null || page.next_cursor <= cursor) {
7820
+ throw new CollectionChangedWhilePagingError(label, `cursor did not advance from ${cursor}`);
7821
+ }
7822
+ cursor = page.next_cursor;
7823
+ }
7824
+ } catch (error) {
7825
+ if (!(error instanceof CollectionChangedWhilePagingError))
7826
+ throw error;
7827
+ lastError = error;
7818
7828
  }
7819
- };
7820
- }
7821
- function getApiKeyStore() {
7822
- if (cachedStore)
7823
- return cachedStore;
7824
- cachedStore = new ApiKeyStore(authClient());
7825
- return cachedStore;
7829
+ }
7830
+ throw lastError ?? new CollectionChangedWhilePagingError(label, "read did not stabilize");
7826
7831
  }
7827
- async function ensureCloudSchema() {
7828
- if (schemaEnsured)
7829
- return schemaEnsured;
7830
- schemaEnsured = (async () => {
7831
- const client = getClient();
7832
- for (const sql of instructionsSchemaSql()) {
7833
- await client.execute(sql);
7834
- }
7835
- await getApiKeyStore().ensureSchema();
7836
- })().catch((e) => {
7837
- schemaEnsured = null;
7838
- throw e;
7839
- });
7840
- return schemaEnsured;
7832
+ class StoreValidationError extends Error {
7833
+ code;
7834
+ constructor(message, code) {
7835
+ super(message);
7836
+ this.code = code;
7837
+ this.name = "StoreValidationError";
7838
+ }
7841
7839
  }
7842
- var honoMiddlewareCache = new Map;
7843
- function getHonoAuthMiddleware(requiredScopes) {
7844
- const key = requiredScopes.join(",");
7845
- const cached2 = honoMiddlewareCache.get(key);
7846
- if (cached2)
7847
- return cached2;
7848
- const signingSecret = resolveSigningSecret();
7849
- if (!signingSecret) {
7850
- throw new Error("Cloud /v1 auth requires a signing secret (HASNA_INSTRUCTIONS_API_SIGNING_KEY / HASNA_API_SIGNING_KEY / API_KEY_SIGNING_SECRET).");
7840
+
7841
+ class IdempotencyConflictError extends Error {
7842
+ code = "IDEMPOTENCY_KEY_REUSED";
7843
+ constructor() {
7844
+ super("Idempotency-Key was already used for a different request body");
7845
+ this.name = "IdempotencyConflictError";
7846
+ }
7847
+ }
7848
+ var IDEMPOTENCY_SCHEMA_SQL = `CREATE TABLE IF NOT EXISTS instruction_idempotency_receipts (
7849
+ principal TEXT NOT NULL,
7850
+ operation TEXT NOT NULL,
7851
+ idempotency_key TEXT NOT NULL,
7852
+ request_sha256 TEXT NOT NULL,
7853
+ response_status INTEGER,
7854
+ response_body JSONB,
7855
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
7856
+ completed_at TIMESTAMPTZ,
7857
+ PRIMARY KEY (principal, operation, idempotency_key),
7858
+ CHECK (length(principal) BETWEEN 1 AND 512),
7859
+ CHECK (length(operation) BETWEEN 1 AND 255),
7860
+ CHECK (length(idempotency_key) BETWEEN 1 AND 255),
7861
+ CHECK (length(request_sha256) = 64),
7862
+ CHECK (
7863
+ (response_status IS NULL AND response_body IS NULL AND completed_at IS NULL)
7864
+ OR
7865
+ (response_status BETWEEN 200 AND 599 AND response_body IS NOT NULL AND completed_at IS NOT NULL)
7866
+ )
7867
+ )`;
7868
+ function canonicalJson(value) {
7869
+ if (value === null || typeof value === "string" || typeof value === "boolean")
7870
+ return JSON.stringify(value);
7871
+ if (typeof value === "number")
7872
+ return JSON.stringify(value);
7873
+ if (Array.isArray(value))
7874
+ return `[${value.map(canonicalJson).join(",")}]`;
7875
+ if (typeof value === "object") {
7876
+ const record = value;
7877
+ return `{${Object.keys(record).sort().filter((key) => record[key] !== undefined).map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
7851
7878
  }
7852
- const store = getApiKeyStore();
7853
- const mw = honoApiKey({
7854
- app: INSTRUCTIONS_APP_SLUG,
7855
- signingSecret,
7856
- keyStatus: store.keyStatus,
7857
- requiredScopes
7858
- });
7859
- honoMiddlewareCache.set(key, mw);
7860
- return mw;
7879
+ return JSON.stringify(null);
7861
7880
  }
7862
- async function pingCloud() {
7863
- const client = getClient();
7864
- const row = await client.get("SELECT 1 AS ok");
7865
- return row?.ok === 1;
7881
+ function idempotencyBodyDigest(body) {
7882
+ return createHash2("sha256").update(canonicalJson(body)).digest("hex");
7866
7883
  }
7867
- async function closeCloud() {
7868
- if (cachedClient)
7869
- await cachedClient.close();
7870
- cachedClient = null;
7871
- cachedStore = null;
7872
- cachedVerifier = null;
7873
- honoMiddlewareCache.clear();
7874
- schemaEnsured = null;
7875
- honoMiddlewareCache.clear();
7884
+ function hasTransaction(client) {
7885
+ return typeof client.transaction === "function";
7886
+ }
7887
+ var idempotencySchemaReady = new WeakSet;
7888
+ async function ensureIdempotencySchema(client) {
7889
+ if (idempotencySchemaReady.has(client))
7890
+ return;
7891
+ await client.execute(IDEMPOTENCY_SCHEMA_SQL);
7892
+ await client.execute("CREATE INDEX IF NOT EXISTS instruction_idempotency_receipts_created_at_idx ON instruction_idempotency_receipts (created_at)");
7893
+ idempotencySchemaReady.add(client);
7894
+ }
7895
+ async function executeIdempotentRequest(client, input, perform) {
7896
+ if (!hasTransaction(client)) {
7897
+ throw new Error("durable idempotency requires a transactional PostgreSQL client");
7898
+ }
7899
+ await ensureIdempotencySchema(client);
7900
+ const digest = idempotencyBodyDigest(input.body);
7901
+ return client.transaction(async (tx) => {
7902
+ const identity = [input.principal, input.operation, input.key];
7903
+ const inserted = await tx.query(`INSERT INTO instruction_idempotency_receipts
7904
+ (principal, operation, idempotency_key, request_sha256)
7905
+ VALUES ($1,$2,$3,$4)
7906
+ ON CONFLICT (principal, operation, idempotency_key) DO NOTHING
7907
+ RETURNING true AS inserted`, [...identity, digest]);
7908
+ const receipt = await tx.get(`SELECT request_sha256, response_status, response_body
7909
+ FROM instruction_idempotency_receipts
7910
+ WHERE principal = $1 AND operation = $2 AND idempotency_key = $3
7911
+ FOR UPDATE`, identity);
7912
+ if (!receipt)
7913
+ throw new Error("idempotency receipt disappeared during transaction");
7914
+ if (receipt.request_sha256 !== digest)
7915
+ throw new IdempotencyConflictError;
7916
+ if (inserted.rowCount === 0) {
7917
+ if (receipt.response_status === null || receipt.response_body === null) {
7918
+ throw new Error("idempotency receipt is incomplete after serialization");
7919
+ }
7920
+ return { status: Number(receipt.response_status), body: receipt.response_body, replayed: true };
7921
+ }
7922
+ const response = await perform(tx);
7923
+ await tx.execute(`UPDATE instruction_idempotency_receipts
7924
+ SET response_status = $5, response_body = $6::jsonb, completed_at = now()
7925
+ WHERE principal = $1 AND operation = $2 AND idempotency_key = $3 AND request_sha256 = $4`, [...identity, digest, response.status, JSON.stringify(response.body)]);
7926
+ return { ...response, replayed: false };
7927
+ });
7876
7928
  }
7877
-
7878
- // src/storage/cloud-store.ts
7879
- init_types();
7880
- init_bounded_read();
7881
- init_instruction_graph();
7882
- init_asset_plan();
7883
- init_machine();
7884
- import { randomUUID } from "crypto";
7885
7929
  function slugify(name) {
7886
7930
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
7887
7931
  }
@@ -7949,7 +7993,7 @@ async function uniqueSlug(client, name, excludeId) {
7949
7993
  }
7950
7994
  throw new Error(`could not allocate a unique slug for '${name}'`);
7951
7995
  }
7952
- async function listConfigs(client, filter = {}) {
7996
+ function configFilterSql(filter) {
7953
7997
  const conditions = [];
7954
7998
  const params = [];
7955
7999
  const add = (sql, value) => {
@@ -7966,12 +8010,37 @@ async function listConfigs(client, filter = {}) {
7966
8010
  add("is_template = $?", filter.is_template);
7967
8011
  if (filter.search) {
7968
8012
  params.push(`%${filter.search}%`);
7969
- const p = `$${params.length}`;
7970
- conditions.push(`(name ILIKE ${p} OR description ILIKE ${p} OR content ILIKE ${p})`);
8013
+ const parameter = `$${params.length}`;
8014
+ conditions.push(`(name ILIKE ${parameter} OR description ILIKE ${parameter} OR content ILIKE ${parameter})`);
7971
8015
  }
7972
- const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
7973
- const rows = await client.many(`SELECT * FROM configs ${where} ORDER BY category, name`, params);
7974
- return rows.map(rowToConfig);
8016
+ return {
8017
+ where: conditions.length ? `WHERE ${conditions.join(" AND ")}` : "",
8018
+ params
8019
+ };
8020
+ }
8021
+ async function listConfigsPage(client, filter = {}, options = {}) {
8022
+ const normalized = normalizeBoundedReadOptions(options);
8023
+ const { where, params } = configFilterSql(filter);
8024
+ const count = await client.get(`SELECT COUNT(*) AS total FROM configs ${where}`, params);
8025
+ const rows = await client.many(`SELECT * FROM configs ${where} ORDER BY id LIMIT $${params.length + 1} OFFSET $${params.length + 2}`, [...params, normalized.limit, normalized.cursor]);
8026
+ return boundedReadPage(rows.map(rowToConfig), Number(count?.total ?? 0), normalized);
8027
+ }
8028
+ async function listConfigIdentitiesPage(client, filterOrOptions = {}, maybeOptions) {
8029
+ const filterKeys = ["category", "agent", "kind", "is_template", "search", "tags"];
8030
+ const isFilterOnly = maybeOptions === undefined && filterKeys.some((key) => (key in filterOrOptions));
8031
+ const filter = maybeOptions === undefined && !isFilterOnly ? {} : filterOrOptions;
8032
+ const options = maybeOptions === undefined && !isFilterOnly ? filterOrOptions : maybeOptions ?? {};
8033
+ const normalized = normalizeBoundedReadOptions(options);
8034
+ const { where, params } = configFilterSql(filter);
8035
+ const count = await client.get(`SELECT COUNT(*) AS total FROM configs ${where}`, params);
8036
+ const rows = await client.many(`SELECT id, name, slug, kind, category, agent, format, is_template, version, created_at, updated_at, synced_at
8037
+ FROM configs ${where} ORDER BY id LIMIT $${params.length + 1} OFFSET $${params.length + 2}`, [...params, normalized.limit, normalized.cursor]);
8038
+ return boundedReadPage(rows.map((row) => ({
8039
+ ...row,
8040
+ created_at: toIso2(row.created_at),
8041
+ updated_at: toIso2(row.updated_at),
8042
+ synced_at: row.synced_at == null ? null : toIso2(row.synced_at)
8043
+ })), Number(count?.total ?? 0), normalized);
7975
8044
  }
7976
8045
  async function getConfig(client, idOrSlug) {
7977
8046
  const row = await client.get("SELECT * FROM configs WHERE id = $1 OR slug = $1", [idOrSlug]);
@@ -7980,10 +8049,10 @@ async function getConfig(client, idOrSlug) {
7980
8049
  return rowToConfig(row);
7981
8050
  }
7982
8051
  async function createConfig(client, input) {
7983
- if (!input.name || !input.name.trim())
7984
- throw new Error("name is required");
7985
- if (!input.category)
7986
- throw new Error("category is required");
8052
+ if (typeof input.name !== "string" || !input.name.trim())
8053
+ throw new StoreValidationError("name is required", "NAME_REQUIRED");
8054
+ if (typeof input.category !== "string" || !input.category.trim())
8055
+ throw new StoreValidationError("category is required", "CATEGORY_REQUIRED");
7987
8056
  const id = randomUUID();
7988
8057
  const slug = await uniqueSlug(client, input.name);
7989
8058
  const snapshotId = randomUUID();
@@ -8092,10 +8161,20 @@ async function createSnapshotContent(client, idOrSlug, content, version) {
8092
8161
  throw new Error("snapshot insert failed");
8093
8162
  return { id: row.id, config_id: row.config_id, content: row.content, version: Number(row.version), created_at: toIso2(row.created_at) };
8094
8163
  }
8095
- async function listSnapshots(client, idOrSlug) {
8164
+ async function listSnapshotsPage(client, idOrSlug, options = {}) {
8096
8165
  const config = await getConfig(client, idOrSlug);
8097
- const rows = await client.many("SELECT id, config_id, content, version, created_at FROM config_snapshots WHERE config_id = $1 ORDER BY version DESC", [config.id]);
8098
- return rows.map((r) => ({ id: r.id, config_id: r.config_id, content: r.content, version: Number(r.version), created_at: toIso2(r.created_at) }));
8166
+ const normalized = normalizeBoundedReadOptions(options);
8167
+ const count = await client.get("SELECT COUNT(*) AS total FROM config_snapshots WHERE config_id = $1", [config.id]);
8168
+ const rows = await client.many(`SELECT id, config_id, content, version, created_at
8169
+ FROM config_snapshots WHERE config_id = $1
8170
+ ORDER BY version DESC, id LIMIT $2 OFFSET $3`, [config.id, normalized.limit, normalized.cursor]);
8171
+ return boundedReadPage(rows.map((row) => ({
8172
+ id: row.id,
8173
+ config_id: row.config_id,
8174
+ content: row.content,
8175
+ version: Number(row.version),
8176
+ created_at: toIso2(row.created_at)
8177
+ })), Number(count?.total ?? 0), normalized);
8099
8178
  }
8100
8179
  async function getSnapshotById(client, snapshotId) {
8101
8180
  const row = await client.get("SELECT id, config_id, content, version, created_at FROM config_snapshots WHERE id = $1", [snapshotId]);
@@ -8125,40 +8204,31 @@ function rowToProfile(row) {
8125
8204
  updated_at: toIso2(row.updated_at)
8126
8205
  };
8127
8206
  }
8128
- async function listProfiles(client) {
8129
- const profiles = [];
8130
- let cursor = 0;
8131
- while (true) {
8132
- const page = await listProfilesPage(client, { limit: 100, cursor });
8133
- profiles.push(...page.items);
8134
- if (page.complete)
8135
- return profiles;
8136
- cursor = page.next_cursor;
8137
- }
8138
- }
8139
8207
  async function listProfilesPage(client, options = {}) {
8140
8208
  const normalized = normalizeBoundedReadOptions(options);
8141
8209
  const count = await client.get("SELECT COUNT(*) AS total FROM profiles");
8142
- const rows = await client.many("SELECT * FROM profiles ORDER BY name LIMIT $1 OFFSET $2", [normalized.limit, normalized.cursor]);
8210
+ const rows = await client.many("SELECT * FROM profiles ORDER BY id LIMIT $1 OFFSET $2", [normalized.limit, normalized.cursor]);
8143
8211
  return boundedReadPage(rows.map(rowToProfile), Number(count?.total ?? 0), normalized);
8144
8212
  }
8213
+ async function listProfileIdentitiesPage(client, options = {}) {
8214
+ const normalized = normalizeBoundedReadOptions(options);
8215
+ const count = await client.get("SELECT COUNT(*) AS total FROM profiles");
8216
+ const rows = await client.many(`SELECT id, name, slug, created_at, updated_at
8217
+ FROM profiles ORDER BY id LIMIT $1 OFFSET $2`, [normalized.limit, normalized.cursor]);
8218
+ return boundedReadPage(rows.map((row) => ({
8219
+ id: row.id,
8220
+ name: row.name,
8221
+ slug: row.slug,
8222
+ created_at: toIso2(row.created_at),
8223
+ updated_at: toIso2(row.updated_at)
8224
+ })), Number(count?.total ?? 0), normalized);
8225
+ }
8145
8226
  async function getProfile(client, idOrSlug) {
8146
8227
  const row = await client.get("SELECT * FROM profiles WHERE id = $1 OR slug = $1", [idOrSlug]);
8147
8228
  if (!row)
8148
8229
  throw new ProfileNotFoundError(idOrSlug);
8149
8230
  return rowToProfile(row);
8150
8231
  }
8151
- async function getProfileConfigs(client, idOrSlug) {
8152
- const configs = [];
8153
- let cursor = 0;
8154
- while (true) {
8155
- const page = await getProfileConfigsPage(client, idOrSlug, { limit: 100, cursor });
8156
- configs.push(...page.items);
8157
- if (page.complete)
8158
- return configs;
8159
- cursor = page.next_cursor;
8160
- }
8161
- }
8162
8232
  async function getProfileConfigsPage(client, idOrSlug, options = {}) {
8163
8233
  const profile = await getProfile(client, idOrSlug);
8164
8234
  const normalized = normalizeBoundedReadOptions(options);
@@ -8166,13 +8236,13 @@ async function getProfileConfigsPage(client, idOrSlug, options = {}) {
8166
8236
  const rows = await client.many(`SELECT c.* FROM configs c
8167
8237
  JOIN profile_configs pc ON pc.config_id = c.id
8168
8238
  WHERE pc.profile_id = $1
8169
- ORDER BY pc.sort_order
8239
+ ORDER BY pc.sort_order, c.id
8170
8240
  LIMIT $2 OFFSET $3`, [profile.id, normalized.limit, normalized.cursor]);
8171
8241
  return boundedReadPage(rows.map(rowToConfig), Number(count?.total ?? 0), normalized);
8172
8242
  }
8173
8243
  async function createProfile(client, input) {
8174
- if (!input.name || !input.name.trim())
8175
- throw new Error("name is required");
8244
+ if (typeof input.name !== "string" || !input.name.trim())
8245
+ throw new StoreValidationError("name is required", "NAME_REQUIRED");
8176
8246
  const id = randomUUID();
8177
8247
  const slug = await uniqueProfileSlug(client, input.name);
8178
8248
  await client.execute(`INSERT INTO profiles (id, name, slug, description, selectors, variables, created_at, updated_at)
@@ -8224,18 +8294,32 @@ async function addConfigToProfile(client, profileIdOrSlug, configId) {
8224
8294
  await client.execute(`INSERT INTO profile_configs (profile_id, config_id, sort_order)
8225
8295
  VALUES ($1,$2,$3) ON CONFLICT (profile_id, config_id) DO NOTHING`, [profile.id, configId, order2]);
8226
8296
  }
8227
- async function getProfileConfigBindings(client, profileIdOrSlug) {
8228
- const profile = await getProfile(client, profileIdOrSlug);
8229
- const rows = await client.many(`SELECT profile_id, config_id, sort_order, binding
8230
- FROM profile_configs
8231
- WHERE profile_id = $1
8232
- ORDER BY sort_order, config_id`, [profile.id]);
8233
- return rows.map((row) => ({
8297
+ function rowToProfileConfigBinding(row) {
8298
+ return {
8234
8299
  profile_id: row.profile_id,
8235
8300
  config_id: row.config_id,
8236
8301
  sort_order: Number(row.sort_order),
8237
8302
  binding: row.binding == null ? legacyProfileConfigBinding() : normalizeProfileConfigBinding(row.binding)
8238
- }));
8303
+ };
8304
+ }
8305
+ async function getProfileConfigBindingsPage(client, profileIdOrSlug, options = {}) {
8306
+ const profile = await getProfile(client, profileIdOrSlug);
8307
+ const normalized = normalizeBoundedReadOptions(options);
8308
+ const count = await client.get("SELECT COUNT(*) AS total FROM profile_configs WHERE profile_id = $1", [profile.id]);
8309
+ const rows = await client.many(`SELECT profile_id, config_id, sort_order, binding
8310
+ FROM profile_configs
8311
+ WHERE profile_id = $1
8312
+ ORDER BY sort_order, config_id
8313
+ LIMIT $2 OFFSET $3`, [profile.id, normalized.limit, normalized.cursor]);
8314
+ return boundedReadPage(rows.map(rowToProfileConfigBinding), Number(count?.total ?? 0), normalized);
8315
+ }
8316
+ async function getProfileConfigBinding(client, profileId, configId) {
8317
+ const row = await client.get(`SELECT profile_id, config_id, sort_order, binding
8318
+ FROM profile_configs
8319
+ WHERE profile_id = $1 AND config_id = $2`, [profileId, configId]);
8320
+ if (!row)
8321
+ throw new Error(`Config ${configId} is not a member of profile ${profileId}.`);
8322
+ return rowToProfileConfigBinding(row);
8239
8323
  }
8240
8324
  async function setProfileConfigBinding(client, profileIdOrSlug, configId, binding) {
8241
8325
  const profile = await getProfile(client, profileIdOrSlug);
@@ -8243,12 +8327,39 @@ async function setProfileConfigBinding(client, profileIdOrSlug, configId, bindin
8243
8327
  const result = await client.query("UPDATE profile_configs SET binding = $1 WHERE profile_id = $2 AND config_id = $3", [JSON.stringify(normalized), profile.id, configId]);
8244
8328
  if ((result.rowCount ?? 0) !== 1)
8245
8329
  throw new Error(`Config ${configId} is not a member of profile ${profile.slug}.`);
8246
- return (await getProfileConfigBindings(client, profile.id)).find((row) => row.config_id === configId);
8330
+ return getProfileConfigBinding(client, profile.id, configId);
8247
8331
  }
8248
8332
  async function removeConfigFromProfile(client, profileIdOrSlug, configId) {
8249
8333
  const profile = await getProfile(client, profileIdOrSlug);
8250
8334
  await client.execute("DELETE FROM profile_configs WHERE profile_id = $1 AND config_id = $2", [profile.id, configId]);
8251
8335
  }
8336
+ function rowToProfileAssetBinding(row) {
8337
+ return {
8338
+ profile_id: row.profile_id,
8339
+ source_config_id: row.source_config_id,
8340
+ sort_order: Number(row.sort_order),
8341
+ binding: normalizeProfileAssetBinding(row.binding)
8342
+ };
8343
+ }
8344
+ async function getProfileAssetBindingsPage(client, profileIdOrSlug, options = {}) {
8345
+ const profile = await getProfile(client, profileIdOrSlug);
8346
+ const normalized = normalizeBoundedReadOptions(options);
8347
+ const count = await client.get("SELECT COUNT(*) AS total FROM profile_assets WHERE profile_id = $1", [profile.id]);
8348
+ const rows = await client.many(`SELECT profile_id, source_config_id, sort_order, binding
8349
+ FROM profile_assets
8350
+ WHERE profile_id = $1
8351
+ ORDER BY sort_order, asset_key
8352
+ LIMIT $2 OFFSET $3`, [profile.id, normalized.limit, normalized.cursor]);
8353
+ return boundedReadPage(rows.map(rowToProfileAssetBinding), Number(count?.total ?? 0), normalized);
8354
+ }
8355
+ async function getProfileAssetBinding(client, profileId, assetKey) {
8356
+ const row = await client.get(`SELECT profile_id, source_config_id, sort_order, binding
8357
+ FROM profile_assets
8358
+ WHERE profile_id = $1 AND asset_key = $2`, [profileId, assetKey]);
8359
+ if (!row)
8360
+ throw new Error(`Asset ${assetKey} is not a member of profile ${profileId}.`);
8361
+ return rowToProfileAssetBinding(row);
8362
+ }
8252
8363
  async function addAssetToProfile(client, profileIdOrSlug, sourceConfigId, binding) {
8253
8364
  const profile = await getProfile(client, profileIdOrSlug);
8254
8365
  await getConfig(client, sourceConfigId);
@@ -8256,7 +8367,7 @@ async function addAssetToProfile(client, profileIdOrSlug, sourceConfigId, bindin
8256
8367
  const maxRow = await client.get("SELECT MAX(sort_order) AS max_order FROM profile_assets WHERE profile_id = $1", [profile.id]);
8257
8368
  const order2 = (maxRow?.max_order ?? -1) + 1;
8258
8369
  await client.execute("INSERT INTO profile_assets (profile_id, source_config_id, asset_key, sort_order, binding) VALUES ($1,$2,$3,$4,$5::jsonb)", [profile.id, sourceConfigId, normalized.assetKey, order2, JSON.stringify(normalized)]);
8259
- return (await getProfileAssetBindings(client, profile.id)).find((row) => row.binding.assetKey === normalized.assetKey);
8370
+ return getProfileAssetBinding(client, profile.id, normalized.assetKey);
8260
8371
  }
8261
8372
  async function setProfileAssetBinding(client, profileIdOrSlug, assetKey, binding) {
8262
8373
  const profile = await getProfile(client, profileIdOrSlug);
@@ -8266,17 +8377,7 @@ async function setProfileAssetBinding(client, profileIdOrSlug, assetKey, binding
8266
8377
  const result = await client.query("UPDATE profile_assets SET binding = $1::jsonb WHERE profile_id = $2 AND asset_key = $3", [JSON.stringify(normalized), profile.id, assetKey]);
8267
8378
  if ((result.rowCount ?? 0) !== 1)
8268
8379
  throw new Error(`Asset ${assetKey} is not a member of profile ${profile.slug}.`);
8269
- return (await getProfileAssetBindings(client, profile.id)).find((row) => row.binding.assetKey === assetKey);
8270
- }
8271
- async function getProfileAssetBindings(client, profileIdOrSlug) {
8272
- const profile = await getProfile(client, profileIdOrSlug);
8273
- const rows = await client.many("SELECT profile_id, source_config_id, sort_order, binding FROM profile_assets WHERE profile_id = $1 ORDER BY sort_order, asset_key", [profile.id]);
8274
- return rows.map((row) => ({
8275
- profile_id: row.profile_id,
8276
- source_config_id: row.source_config_id,
8277
- sort_order: Number(row.sort_order),
8278
- binding: normalizeProfileAssetBinding(row.binding)
8279
- }));
8380
+ return getProfileAssetBinding(client, profile.id, assetKey);
8280
8381
  }
8281
8382
  async function removeAssetFromProfile(client, profileIdOrSlug, assetKey) {
8282
8383
  const profile = await getProfile(client, profileIdOrSlug);
@@ -8291,39 +8392,33 @@ async function resolveProfileForMachineRead(client, machine, options = {}) {
8291
8392
  const os = (machine.os ?? "").trim().toLowerCase();
8292
8393
  const osFamily = normalizeOsFamily(machine.os);
8293
8394
  const arch = (machine.arch ?? "").trim().toLowerCase();
8294
- let cursor = 0;
8295
- let scanned = 0;
8296
- let total = 0;
8395
+ const readStableProfiles = (queryClient) => aggregateBoundedCollectionRead("profile resolution", (cursor) => listProfilesPage(queryClient, { limit, cursor }), (profile) => profile.id, true, true);
8396
+ const stableRead = hasTransaction(client) ? await client.transaction(async (transaction) => {
8397
+ await transaction.execute("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY");
8398
+ return readStableProfiles(transaction);
8399
+ }) : await readStableProfiles(client);
8297
8400
  let selected = null;
8298
- while (true) {
8299
- const page = await listProfilesPage(client, { limit, cursor });
8300
- total = page.total;
8301
- scanned += page.items.length;
8302
- for (const p of page.items) {
8303
- if (!profileHasSelectors(p.selectors))
8304
- continue;
8305
- const s = p.selectors;
8306
- const osOk = !s.os?.length || s.os.some((c) => {
8307
- const value = c.trim().toLowerCase();
8308
- return value === os || normalizeOsFamily(c) === osFamily;
8309
- });
8310
- const archOk = !s.arch?.length || s.arch.some((c) => c.trim().toLowerCase() === arch);
8311
- const hostOk = !s.hostnames?.length || s.hostnames.some((c) => c.trim().toLowerCase() === host);
8312
- if (!osOk || !archOk || !hostOk)
8313
- continue;
8314
- const score = (p.selectors.hostnames?.length ? 100 : 0) + (p.selectors.os?.length ? 10 : 0) + (p.selectors.arch?.length ? 10 : 0);
8315
- if (!selected || score > selected.score || score === selected.score && p.name.localeCompare(selected.profile.name) < 0) {
8316
- selected = { profile: p, score };
8317
- }
8401
+ for (const p of stableRead.items) {
8402
+ if (!profileHasSelectors(p.selectors))
8403
+ continue;
8404
+ const s = p.selectors;
8405
+ const osOk = !s.os?.length || s.os.some((c) => {
8406
+ const value = c.trim().toLowerCase();
8407
+ return value === os || normalizeOsFamily(c) === osFamily;
8408
+ });
8409
+ const archOk = !s.arch?.length || s.arch.some((c) => c.trim().toLowerCase() === arch);
8410
+ const hostOk = !s.hostnames?.length || s.hostnames.some((c) => c.trim().toLowerCase() === host);
8411
+ if (!osOk || !archOk || !hostOk)
8412
+ continue;
8413
+ const score = (p.selectors.hostnames?.length ? 100 : 0) + (p.selectors.os?.length ? 10 : 0) + (p.selectors.arch?.length ? 10 : 0);
8414
+ if (!selected || score > selected.score || score === selected.score && p.name.localeCompare(selected.profile.name) < 0) {
8415
+ selected = { profile: p, score };
8318
8416
  }
8319
- if (page.complete)
8320
- break;
8321
- cursor = page.next_cursor;
8322
8417
  }
8323
8418
  return {
8324
8419
  profile: selected?.profile ?? null,
8325
- scanned,
8326
- total,
8420
+ scanned: stableRead.items.length,
8421
+ total: stableRead.total,
8327
8422
  batch_limit: limit,
8328
8423
  source_bounded: true,
8329
8424
  complete: true,
@@ -8340,9 +8435,19 @@ function rowToMachine(row) {
8340
8435
  created_at: toIso2(row.created_at)
8341
8436
  };
8342
8437
  }
8343
- async function listMachines(client) {
8344
- const rows = await client.many("SELECT * FROM machines ORDER BY last_applied_at DESC NULLS LAST");
8345
- return rows.map(rowToMachine);
8438
+ async function listMachinesPage(client, options = {}) {
8439
+ const normalized = normalizeBoundedReadOptions(options);
8440
+ const count = await client.get("SELECT COUNT(*) AS total FROM machines");
8441
+ const rows = await client.many(`SELECT * FROM machines
8442
+ ORDER BY id LIMIT $1 OFFSET $2`, [normalized.limit, normalized.cursor]);
8443
+ return boundedReadPage(rows.map(rowToMachine), Number(count?.total ?? 0), normalized);
8444
+ }
8445
+ async function listMachineIdentitiesPage(client, options = {}) {
8446
+ const normalized = normalizeBoundedReadOptions(options);
8447
+ const count = await client.get("SELECT COUNT(*) AS total FROM machines");
8448
+ const rows = await client.many(`SELECT id, hostname, os, arch, last_applied_at, created_at
8449
+ FROM machines ORDER BY id LIMIT $1 OFFSET $2`, [normalized.limit, normalized.cursor]);
8450
+ return boundedReadPage(rows.map(rowToMachine), Number(count?.total ?? 0), normalized);
8346
8451
  }
8347
8452
  async function registerMachine(client, hostname, os, arch) {
8348
8453
  const existing = await client.get("SELECT * FROM machines WHERE hostname = $1", [hostname]);
@@ -8365,38 +8470,229 @@ async function insertFeedback(client, input) {
8365
8470
  await client.execute("INSERT INTO feedback (id, message, email, category, version, created_at) VALUES ($1,$2,$3,$4,$5,now())", [randomUUID(), input.message, input.email ?? null, input.category ?? "general", input.version ?? null]);
8366
8471
  }
8367
8472
 
8368
- // src/server/v1.ts
8369
- var JSON_HEADERS = { "Content-Type": "application/json" };
8370
- function json(body, status = 200) {
8371
- return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
8473
+ // src/server/cloud.ts
8474
+ var INSTRUCTIONS_APP_SLUG = "instructions";
8475
+ function resolveCloudDatabaseUrl(env = process.env) {
8476
+ return env.HASNA_INSTRUCTIONS_DATABASE_URL || env.INSTRUCTIONS_DATABASE_URL || env.DATABASE_URL || undefined;
8372
8477
  }
8373
- function errorResponse(status, message, extra) {
8374
- return json({ error: message, ...extra ?? {} }, status);
8478
+ function resolveSigningSecret(env = process.env) {
8479
+ return env.HASNA_INSTRUCTIONS_API_SIGNING_KEY?.trim() || env.HASNA_API_SIGNING_KEY?.trim() || env.API_KEY_SIGNING_SECRET?.trim() || undefined;
8375
8480
  }
8376
- function completeLegacyPage(items) {
8481
+ function isPostgresBackendEnabled(env = process.env) {
8482
+ return Boolean(resolveCloudDatabaseUrl(env));
8483
+ }
8484
+ var cachedClient = null;
8485
+ var cachedStore = null;
8486
+ var cachedVerifier = null;
8487
+ var schemaEnsured = null;
8488
+ function getClient() {
8489
+ if (cachedClient)
8490
+ return cachedClient;
8491
+ const url = resolveCloudDatabaseUrl();
8492
+ if (!url) {
8493
+ throw new Error("Cloud /v1 requires a remote database URL (HASNA_INSTRUCTIONS_DATABASE_URL / INSTRUCTIONS_DATABASE_URL / DATABASE_URL).");
8494
+ }
8495
+ const pool2 = createPgPool({
8496
+ connectionString: url,
8497
+ max: 6,
8498
+ idleTimeoutMillis: 30000,
8499
+ connectionTimeoutMillis: 15000,
8500
+ applicationName: "instructions-serve"
8501
+ });
8502
+ cachedClient = createQueryClient(pool2);
8503
+ return cachedClient;
8504
+ }
8505
+ function getCloudClient() {
8506
+ return getClient();
8507
+ }
8508
+ function authClient() {
8509
+ const client = getClient();
8377
8510
  return {
8378
- items,
8379
- total: items.length,
8380
- limit: Math.max(items.length, 1),
8381
- cursor: 0,
8382
- next_cursor: null,
8383
- has_more: false,
8384
- complete: true,
8385
- truncated: false,
8386
- source_bounded: false
8511
+ async many(sql, params = []) {
8512
+ return client.many(sql, params);
8513
+ },
8514
+ async get(sql, params = []) {
8515
+ return client.get(sql, params);
8516
+ },
8517
+ async execute(sql, params = []) {
8518
+ await client.execute(sql, params);
8519
+ }
8520
+ };
8521
+ }
8522
+ function getApiKeyStore() {
8523
+ if (cachedStore)
8524
+ return cachedStore;
8525
+ cachedStore = new ApiKeyStore(authClient());
8526
+ return cachedStore;
8527
+ }
8528
+ async function ensureCloudSchema() {
8529
+ if (schemaEnsured)
8530
+ return schemaEnsured;
8531
+ schemaEnsured = (async () => {
8532
+ const client = getClient();
8533
+ for (const sql of instructionsSchemaSql()) {
8534
+ await client.execute(sql);
8535
+ }
8536
+ await ensureIdempotencySchema(client);
8537
+ await getApiKeyStore().ensureSchema();
8538
+ })().catch((e) => {
8539
+ schemaEnsured = null;
8540
+ throw e;
8541
+ });
8542
+ return schemaEnsured;
8543
+ }
8544
+ var honoMiddlewareCache = new Map;
8545
+ function getHonoAuthMiddleware(requiredScopes) {
8546
+ const key = requiredScopes.join(",");
8547
+ const cached2 = honoMiddlewareCache.get(key);
8548
+ if (cached2)
8549
+ return cached2;
8550
+ const signingSecret = resolveSigningSecret();
8551
+ if (!signingSecret) {
8552
+ throw new Error("Cloud /v1 auth requires a signing secret (HASNA_INSTRUCTIONS_API_SIGNING_KEY / HASNA_API_SIGNING_KEY / API_KEY_SIGNING_SECRET).");
8553
+ }
8554
+ const store = getApiKeyStore();
8555
+ const mw = honoApiKey({
8556
+ app: INSTRUCTIONS_APP_SLUG,
8557
+ signingSecret,
8558
+ keyStatus: store.keyStatus,
8559
+ requiredScopes
8560
+ });
8561
+ honoMiddlewareCache.set(key, mw);
8562
+ return mw;
8563
+ }
8564
+ async function pingCloud() {
8565
+ const client = getClient();
8566
+ const row = await client.get("SELECT 1 AS ok");
8567
+ return row?.ok === 1;
8568
+ }
8569
+ async function closeCloud() {
8570
+ if (cachedClient)
8571
+ await cachedClient.close();
8572
+ cachedClient = null;
8573
+ cachedStore = null;
8574
+ cachedVerifier = null;
8575
+ honoMiddlewareCache.clear();
8576
+ schemaEnsured = null;
8577
+ honoMiddlewareCache.clear();
8578
+ }
8579
+
8580
+ // src/server/v1.ts
8581
+ var JSON_HEADERS = { "Content-Type": "application/json" };
8582
+ var MAX_REQUEST_BODY_BYTES = 1024 * 1024;
8583
+ var MAX_SEARCH_QUERY_CHARS = 512;
8584
+ var MAX_IDEMPOTENCY_KEY_CHARS = 255;
8585
+
8586
+ class HttpInputError extends Error {
8587
+ status;
8588
+ code;
8589
+ constructor(status, code, message) {
8590
+ super(message);
8591
+ this.status = status;
8592
+ this.code = code;
8593
+ this.name = "HttpInputError";
8594
+ }
8595
+ }
8596
+ function json(body, status = 200) {
8597
+ return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
8598
+ }
8599
+ function errorResponse(status, message, extra) {
8600
+ return json({ error: message, ...extra ?? {} }, status);
8601
+ }
8602
+ function pagePayload(alias, page) {
8603
+ return {
8604
+ ...page,
8605
+ [alias]: page.items,
8606
+ count: page.items.length
8387
8607
  };
8388
8608
  }
8609
+ function authenticatedPrincipalAuthority(principal) {
8610
+ if (!principal)
8611
+ throw new Error("authenticated principal is unavailable");
8612
+ return [
8613
+ principal.app,
8614
+ `tenant:${principal.tid ?? "-"}`,
8615
+ `agent:${principal.agent ?? "-"}`,
8616
+ `kid:${principal.kid}`
8617
+ ].join("|");
8618
+ }
8619
+ function readIdempotencyKey(req) {
8620
+ const raw2 = req.headers.get("idempotency-key");
8621
+ if (raw2 === null)
8622
+ return null;
8623
+ const key = raw2.trim();
8624
+ if (key.length < 1 || key.length > MAX_IDEMPOTENCY_KEY_CHARS || /[^\x21-\x7e]/.test(key)) {
8625
+ throw new HttpInputError(400, "INVALID_IDEMPOTENCY_KEY", `Idempotency-Key must contain 1-${MAX_IDEMPOTENCY_KEY_CHARS} visible ASCII characters`);
8626
+ }
8627
+ return key;
8628
+ }
8629
+ async function mutationResponse(req, client, context, operation, requestBody, status, perform) {
8630
+ const key = readIdempotencyKey(req);
8631
+ if (!key)
8632
+ return json(await perform(client), status);
8633
+ const result = await executeIdempotentRequest(client, {
8634
+ principal: authenticatedPrincipalAuthority(context.principal),
8635
+ operation,
8636
+ key,
8637
+ body: requestBody
8638
+ }, async (transaction) => ({ status, body: await perform(transaction) }));
8639
+ return json(result.body, result.status);
8640
+ }
8389
8641
  async function readJson(req) {
8642
+ const declaredLength = req.headers.get("content-length");
8643
+ if (declaredLength !== null) {
8644
+ const parsed = Number(declaredLength);
8645
+ if (!Number.isSafeInteger(parsed) || parsed < 0)
8646
+ return null;
8647
+ if (parsed > MAX_REQUEST_BODY_BYTES) {
8648
+ throw new HttpInputError(413, "REQUEST_BODY_TOO_LARGE", "request body exceeds the 1 MiB limit");
8649
+ }
8650
+ }
8651
+ if (!req.body)
8652
+ return {};
8653
+ const reader = req.body.getReader();
8654
+ const chunks = [];
8655
+ let total = 0;
8390
8656
  try {
8391
- const text = await req.text();
8392
- if (!text)
8393
- return {};
8394
- return JSON.parse(text);
8657
+ while (true) {
8658
+ const { done, value } = await reader.read();
8659
+ if (done)
8660
+ break;
8661
+ if (!value)
8662
+ continue;
8663
+ total += value.byteLength;
8664
+ if (total > MAX_REQUEST_BODY_BYTES) {
8665
+ await reader.cancel();
8666
+ throw new HttpInputError(413, "REQUEST_BODY_TOO_LARGE", "request body exceeds the 1 MiB limit");
8667
+ }
8668
+ chunks.push(value);
8669
+ }
8670
+ } finally {
8671
+ reader.releaseLock();
8672
+ }
8673
+ if (total === 0)
8674
+ return {};
8675
+ const bytes = new Uint8Array(total);
8676
+ let offset = 0;
8677
+ for (const chunk of chunks) {
8678
+ bytes.set(chunk, offset);
8679
+ offset += chunk.byteLength;
8680
+ }
8681
+ try {
8682
+ return JSON.parse(new TextDecoder().decode(bytes));
8395
8683
  } catch {
8396
8684
  return null;
8397
8685
  }
8398
8686
  }
8399
- async function handleV1Request(req, url) {
8687
+ function boundedSearchQuery(value) {
8688
+ if (value === null || value === "")
8689
+ return;
8690
+ if (value.length > MAX_SEARCH_QUERY_CHARS) {
8691
+ throw new HttpInputError(400, "SEARCH_QUERY_TOO_LONG", `search query exceeds ${MAX_SEARCH_QUERY_CHARS} characters`);
8692
+ }
8693
+ return value;
8694
+ }
8695
+ async function handleV1Request(req, url, context = {}) {
8400
8696
  const path = url.pathname;
8401
8697
  if (path !== "/v1" && !path.startsWith("/v1/"))
8402
8698
  return null;
@@ -8404,7 +8700,8 @@ async function handleV1Request(req, url) {
8404
8700
  try {
8405
8701
  await ensureCloudSchema();
8406
8702
  } catch (e) {
8407
- return errorResponse(503, `database unavailable: ${e.message}`);
8703
+ console.error("instructions /v1: database unavailable");
8704
+ return errorResponse(503, "Instructions database is unavailable", { code: "DATABASE_UNAVAILABLE" });
8408
8705
  }
8409
8706
  const client = getCloudClient();
8410
8707
  const segments = path.split("/").filter(Boolean);
@@ -8415,25 +8712,31 @@ async function handleV1Request(req, url) {
8415
8712
  if (resource === "configs") {
8416
8713
  if (!id) {
8417
8714
  if (method === "GET") {
8715
+ const search = boundedSearchQuery(url.searchParams.get("search"));
8418
8716
  const filter = {
8419
8717
  ...url.searchParams.get("category") ? { category: url.searchParams.get("category") } : {},
8420
8718
  ...url.searchParams.get("agent") ? { agent: url.searchParams.get("agent") } : {},
8421
8719
  ...url.searchParams.get("kind") ? { kind: url.searchParams.get("kind") } : {},
8422
- ...url.searchParams.get("search") ? { search: url.searchParams.get("search") } : {}
8720
+ ...search ? { search } : {}
8721
+ };
8722
+ const options = {
8723
+ limit: url.searchParams.get("limit") ?? undefined,
8724
+ cursor: url.searchParams.get("cursor") ?? undefined
8423
8725
  };
8424
- const configs = await listConfigs(client, filter);
8425
- return json({ configs, count: configs.length });
8726
+ if (url.searchParams.get("view") === "identity") {
8727
+ const page2 = await listConfigIdentitiesPage(client, filter, options);
8728
+ return json(pagePayload("configs", page2));
8729
+ }
8730
+ const page = await listConfigsPage(client, filter, options);
8731
+ return json(pagePayload("configs", page));
8426
8732
  }
8427
8733
  if (method === "POST") {
8428
8734
  const body = await readJson(req);
8429
8735
  if (!body)
8430
8736
  return errorResponse(400, "invalid JSON body");
8431
- try {
8432
- const config = await createConfig(client, body);
8433
- return json({ config }, 201);
8434
- } catch (e) {
8435
- return errorResponse(400, e.message);
8436
- }
8737
+ return await mutationResponse(req, client, context, "POST /v1/configs", body, 201, async (transaction) => ({
8738
+ config: await createConfig(transaction, body)
8739
+ }));
8437
8740
  }
8438
8741
  return errorResponse(405, `method ${method} not allowed on /v1/configs`);
8439
8742
  }
@@ -8455,13 +8758,17 @@ async function handleV1Request(req, url) {
8455
8758
  return json({ snapshot });
8456
8759
  }
8457
8760
  if (method === "GET") {
8458
- const snapshots = await listSnapshots(client, id);
8459
- return json({ snapshots, count: snapshots.length });
8761
+ const page = await listSnapshotsPage(client, id, {
8762
+ limit: url.searchParams.get("limit") ?? undefined,
8763
+ cursor: url.searchParams.get("cursor") ?? undefined
8764
+ });
8765
+ return json(pagePayload("snapshots", page));
8460
8766
  }
8461
8767
  if (method === "POST") {
8462
8768
  const body = await readJson(req);
8463
- const snapshot = body && typeof body.content === "string" && typeof body.version === "number" ? await createSnapshotContent(client, id, body.content, body.version) : await createSnapshot(client, id);
8464
- return json({ snapshot }, 201);
8769
+ return await mutationResponse(req, client, context, "POST /v1/configs/:id/snapshots", { config_id: id, ...body ?? {} }, 201, async (transaction) => ({
8770
+ snapshot: body && typeof body.content === "string" && typeof body.version === "number" ? await createSnapshotContent(transaction, id, body.content, body.version) : await createSnapshot(transaction, id)
8771
+ }));
8465
8772
  }
8466
8773
  return errorResponse(405, `method ${method} not allowed on /v1/configs/:id/snapshots`);
8467
8774
  }
@@ -8487,26 +8794,24 @@ async function handleV1Request(req, url) {
8487
8794
  if (resource === "profiles") {
8488
8795
  if (!id) {
8489
8796
  if (method === "GET") {
8490
- if (!url.searchParams.has("limit") && !url.searchParams.has("cursor")) {
8491
- const profiles = await listProfiles(client);
8492
- return json({ ...completeLegacyPage(profiles), profiles, count: profiles.length });
8493
- }
8494
- const page = await listProfilesPage(client, {
8797
+ const options = {
8495
8798
  limit: url.searchParams.get("limit") ?? undefined,
8496
8799
  cursor: url.searchParams.get("cursor") ?? undefined
8497
- });
8498
- return json({ ...page, profiles: page.items, count: page.items.length });
8800
+ };
8801
+ if (url.searchParams.get("view") === "identity") {
8802
+ const page2 = await listProfileIdentitiesPage(client, options);
8803
+ return json(pagePayload("profiles", page2));
8804
+ }
8805
+ const page = await listProfilesPage(client, options);
8806
+ return json(pagePayload("profiles", page));
8499
8807
  }
8500
8808
  if (method === "POST") {
8501
8809
  const body = await readJson(req);
8502
8810
  if (!body)
8503
8811
  return errorResponse(400, "invalid JSON body");
8504
- try {
8505
- const profile = await createProfile(client, body);
8506
- return json({ profile }, 201);
8507
- } catch (e) {
8508
- return errorResponse(400, e.message);
8509
- }
8812
+ return await mutationResponse(req, client, context, "POST /v1/profiles", body, 201, async (transaction) => ({
8813
+ profile: await createProfile(transaction, body)
8814
+ }));
8510
8815
  }
8511
8816
  return errorResponse(405, `method ${method} not allowed on /v1/profiles`);
8512
8817
  }
@@ -8523,8 +8828,11 @@ async function handleV1Request(req, url) {
8523
8828
  if (action === "bindings") {
8524
8829
  if (method !== "GET")
8525
8830
  return errorResponse(405, `method ${method} not allowed on /v1/profiles/:id/bindings`);
8526
- const bindings = await getProfileConfigBindings(client, id);
8527
- return json({ bindings });
8831
+ const page = await getProfileConfigBindingsPage(client, id, {
8832
+ limit: url.searchParams.get("limit") ?? undefined,
8833
+ cursor: url.searchParams.get("cursor") ?? undefined
8834
+ });
8835
+ return json(pagePayload("bindings", page));
8528
8836
  }
8529
8837
  if (action === "configs") {
8530
8838
  const configId = segments[4] ? decodeURIComponent(segments[4]) : undefined;
@@ -8532,8 +8840,10 @@ async function handleV1Request(req, url) {
8532
8840
  const body = await readJson(req);
8533
8841
  if (!body?.config_id)
8534
8842
  return errorResponse(400, "config_id is required");
8535
- await addConfigToProfile(client, id, body.config_id);
8536
- return json({ added: true });
8843
+ return await mutationResponse(req, client, context, "POST /v1/profiles/:id/configs", { profile_id: id, config_id: body.config_id }, 200, async (transaction) => {
8844
+ await addConfigToProfile(transaction, id, body.config_id);
8845
+ return { added: true };
8846
+ });
8537
8847
  }
8538
8848
  if (method === "DELETE" && configId) {
8539
8849
  await removeConfigFromProfile(client, id, configId);
@@ -8543,30 +8853,36 @@ async function handleV1Request(req, url) {
8543
8853
  const body = await readJson(req);
8544
8854
  if (!body?.binding)
8545
8855
  return errorResponse(400, "binding is required");
8546
- const binding = await setProfileConfigBinding(client, id, configId, body.binding);
8547
- return json({ binding });
8856
+ return await mutationResponse(req, client, context, "PUT /v1/profiles/:id/configs/:configId", { profile_id: id, config_id: configId, binding: body.binding }, 200, async (transaction) => ({
8857
+ binding: await setProfileConfigBinding(transaction, id, configId, body.binding)
8858
+ }));
8548
8859
  }
8549
8860
  return errorResponse(405, `method ${method} not allowed on /v1/profiles/:id/configs`);
8550
8861
  }
8551
8862
  if (action === "assets") {
8552
8863
  const assetKey = segments[4] ? decodeURIComponent(segments[4]) : undefined;
8553
8864
  if (method === "GET" && !assetKey) {
8554
- const assets = await getProfileAssetBindings(client, id);
8555
- return json({ assets });
8865
+ const page = await getProfileAssetBindingsPage(client, id, {
8866
+ limit: url.searchParams.get("limit") ?? undefined,
8867
+ cursor: url.searchParams.get("cursor") ?? undefined
8868
+ });
8869
+ return json(pagePayload("assets", page));
8556
8870
  }
8557
8871
  if (method === "POST" && !assetKey) {
8558
8872
  const body = await readJson(req);
8559
8873
  if (!body?.source_config_id || !body.binding)
8560
8874
  return errorResponse(400, "source_config_id and binding are required");
8561
- const asset = await addAssetToProfile(client, id, body.source_config_id, body.binding);
8562
- return json({ asset }, 201);
8875
+ return await mutationResponse(req, client, context, "POST /v1/profiles/:id/assets", { profile_id: id, source_config_id: body.source_config_id, binding: body.binding }, 201, async (transaction) => ({
8876
+ asset: await addAssetToProfile(transaction, id, body.source_config_id, body.binding)
8877
+ }));
8563
8878
  }
8564
8879
  if (method === "PUT" && assetKey) {
8565
8880
  const body = await readJson(req);
8566
8881
  if (!body?.binding)
8567
8882
  return errorResponse(400, "binding is required");
8568
- const asset = await setProfileAssetBinding(client, id, assetKey, body.binding);
8569
- return json({ asset });
8883
+ return await mutationResponse(req, client, context, "PUT /v1/profiles/:id/assets/:assetKey", { profile_id: id, asset_key: assetKey, binding: body.binding }, 200, async (transaction) => ({
8884
+ asset: await setProfileAssetBinding(transaction, id, assetKey, body.binding)
8885
+ }));
8570
8886
  }
8571
8887
  if (method === "DELETE" && assetKey) {
8572
8888
  await removeAssetFromProfile(client, id, assetKey);
@@ -8578,13 +8894,6 @@ async function handleV1Request(req, url) {
8578
8894
  return errorResponse(404, `unknown profile action: ${action}`);
8579
8895
  if (method === "GET") {
8580
8896
  const profile = await getProfile(client, id);
8581
- if (!url.searchParams.has("limit") && !url.searchParams.has("cursor")) {
8582
- const legacyConfigs = await getProfileConfigs(client, id);
8583
- return json({
8584
- profile: { ...profile, configs: legacyConfigs },
8585
- configs: completeLegacyPage(legacyConfigs)
8586
- });
8587
- }
8588
8897
  const configs = await getProfileConfigsPage(client, id, {
8589
8898
  limit: url.searchParams.get("limit") ?? undefined,
8590
8899
  cursor: url.searchParams.get("cursor") ?? undefined
@@ -8629,15 +8938,20 @@ async function handleV1Request(req, url) {
8629
8938
  }
8630
8939
  if (!id) {
8631
8940
  if (method === "GET") {
8632
- const machines = await listMachines(client);
8633
- return json({ machines, count: machines.length });
8941
+ const options = {
8942
+ limit: url.searchParams.get("limit") ?? undefined,
8943
+ cursor: url.searchParams.get("cursor") ?? undefined
8944
+ };
8945
+ const page = url.searchParams.get("view") === "identity" ? await listMachineIdentitiesPage(client, options) : await listMachinesPage(client, options);
8946
+ return json(pagePayload("machines", page));
8634
8947
  }
8635
8948
  if (method === "POST") {
8636
8949
  const body = await readJson(req);
8637
8950
  if (!body?.hostname)
8638
8951
  return errorResponse(400, "hostname is required");
8639
- const machine = await registerMachine(client, body.hostname, body.os ?? null, body.arch ?? null);
8640
- return json({ machine }, 201);
8952
+ return await mutationResponse(req, client, context, "POST /v1/machines", body, 201, async (transaction) => ({
8953
+ machine: await registerMachine(transaction, body.hostname, body.os ?? null, body.arch ?? null)
8954
+ }));
8641
8955
  }
8642
8956
  return errorResponse(405, `method ${method} not allowed on /v1/machines`);
8643
8957
  }
@@ -8659,10 +8973,17 @@ async function handleV1Request(req, url) {
8659
8973
  }
8660
8974
  return errorResponse(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
8661
8975
  } catch (e) {
8976
+ if (e instanceof HttpInputError)
8977
+ return errorResponse(e.status, e.message, { code: e.code });
8978
+ if (e instanceof StoreValidationError)
8979
+ return errorResponse(400, e.message, { code: e.code });
8980
+ if (e instanceof IdempotencyConflictError)
8981
+ return errorResponse(409, e.message, { code: e.code });
8662
8982
  if (e instanceof ConfigNotFoundError || e instanceof ProfileNotFoundError) {
8663
8983
  return errorResponse(404, e.message);
8664
8984
  }
8665
- return errorResponse(500, e.message || "internal error");
8985
+ console.error("instructions /v1: request failed");
8986
+ return errorResponse(500, "Instructions request failed", { code: "INTERNAL_ERROR" });
8666
8987
  }
8667
8988
  }
8668
8989
 
@@ -8702,6 +9023,88 @@ var profileSchema = {
8702
9023
  updated_at: { type: "string" }
8703
9024
  }
8704
9025
  };
9026
+ var configIdentitySchema = {
9027
+ type: "object",
9028
+ required: ["id", "name", "slug", "kind", "category", "agent", "format", "is_template", "version", "created_at", "updated_at", "synced_at"],
9029
+ properties: {
9030
+ id: { type: "string" },
9031
+ name: { type: "string" },
9032
+ slug: { type: "string" },
9033
+ kind: { type: "string" },
9034
+ category: { type: "string" },
9035
+ agent: { type: "string" },
9036
+ format: { type: "string" },
9037
+ is_template: { type: "boolean" },
9038
+ version: { type: "number" },
9039
+ created_at: { type: "string" },
9040
+ updated_at: { type: "string" },
9041
+ synced_at: { type: "string", nullable: true }
9042
+ }
9043
+ };
9044
+ var profileIdentitySchema = {
9045
+ type: "object",
9046
+ required: ["id", "name", "slug", "created_at", "updated_at"],
9047
+ properties: {
9048
+ id: { type: "string" },
9049
+ name: { type: "string" },
9050
+ slug: { type: "string" },
9051
+ created_at: { type: "string" },
9052
+ updated_at: { type: "string" }
9053
+ }
9054
+ };
9055
+ var machineSchema = {
9056
+ type: "object",
9057
+ required: ["id", "hostname", "os", "arch", "last_applied_at", "created_at"],
9058
+ properties: {
9059
+ id: { type: "string" },
9060
+ hostname: { type: "string" },
9061
+ os: { type: "string", nullable: true },
9062
+ arch: { type: "string", nullable: true },
9063
+ last_applied_at: { type: "string", nullable: true },
9064
+ created_at: { type: "string" }
9065
+ }
9066
+ };
9067
+ var snapshotSchema = {
9068
+ type: "object",
9069
+ required: ["id", "config_id", "content", "version", "created_at"],
9070
+ properties: {
9071
+ id: { type: "string" },
9072
+ config_id: { type: "string" },
9073
+ content: { type: "string" },
9074
+ version: { type: "number" },
9075
+ created_at: { type: "string" }
9076
+ }
9077
+ };
9078
+ var limitParameter = { name: "limit", in: "query", schema: { type: "integer", minimum: 1, maximum: 100 } };
9079
+ var cursorParameter = { name: "cursor", in: "query", schema: { type: "integer", minimum: 0, maximum: 1e5 } };
9080
+ var identityViewParameter = {
9081
+ name: "view",
9082
+ in: "query",
9083
+ description: "Use identity for a metadata-only projection that omits instruction content, paths, outputs, and private profile fields.",
9084
+ schema: { type: "string", enum: ["identity"] }
9085
+ };
9086
+ var idempotencyKeyParameter = {
9087
+ name: "Idempotency-Key",
9088
+ in: "header",
9089
+ required: false,
9090
+ description: "Optional visible-ASCII request key (1-255 characters). Replays the first committed response for the same authenticated principal, operation, and canonical JSON body; reuse with a different body returns 409.",
9091
+ schema: { type: "string", minLength: 1, maxLength: 255, pattern: "^[!-~]+$" }
9092
+ };
9093
+ var idempotencyConflictResponse = {
9094
+ description: "The Idempotency-Key was already used for a different request body.",
9095
+ content: {
9096
+ "application/json": {
9097
+ schema: {
9098
+ type: "object",
9099
+ required: ["error", "code"],
9100
+ properties: {
9101
+ error: { type: "string" },
9102
+ code: { type: "string", const: "IDEMPOTENCY_KEY_REUSED" }
9103
+ }
9104
+ }
9105
+ }
9106
+ }
9107
+ };
8705
9108
  function buildV1OpenApiDocument(version = getPackageVersion()) {
8706
9109
  return {
8707
9110
  openapi: "3.1.0",
@@ -8717,7 +9120,11 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
8717
9120
  },
8718
9121
  schemas: {
8719
9122
  Config: configSchema,
9123
+ ConfigIdentity: configIdentitySchema,
8720
9124
  Profile: profileSchema,
9125
+ ProfileIdentity: profileIdentitySchema,
9126
+ Machine: machineSchema,
9127
+ ConfigSnapshot: snapshotSchema,
8721
9128
  CreateConfigInput: {
8722
9129
  type: "object",
8723
9130
  required: ["name", "category", "content"],
@@ -8738,12 +9145,17 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
8738
9145
  type: "object",
8739
9146
  properties: {
8740
9147
  name: { type: "string" },
9148
+ kind: { type: "string" },
8741
9149
  category: { type: "string" },
8742
9150
  agent: { type: "string" },
9151
+ target_path: { type: "string", nullable: true },
9152
+ outputs: { type: "array", items: { type: "object" } },
9153
+ format: { type: "string" },
8743
9154
  content: { type: "string" },
8744
- description: { type: "string" },
9155
+ description: { type: "string", nullable: true },
8745
9156
  tags: { type: "array", items: { type: "string" } },
8746
- is_template: { type: "boolean" }
9157
+ is_template: { type: "boolean" },
9158
+ synced_at: { type: "string", nullable: true }
8747
9159
  }
8748
9160
  },
8749
9161
  CreateProfileInput: {
@@ -8756,6 +9168,38 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
8756
9168
  variables: { type: "object" }
8757
9169
  }
8758
9170
  },
9171
+ UpdateProfileInput: {
9172
+ type: "object",
9173
+ properties: {
9174
+ name: { type: "string" },
9175
+ description: { type: "string", nullable: true },
9176
+ selectors: { type: "object" },
9177
+ variables: { type: "object" }
9178
+ }
9179
+ },
9180
+ PruneSnapshotsInput: {
9181
+ type: "object",
9182
+ properties: {
9183
+ keep: { type: "integer", minimum: 0, default: 10 }
9184
+ }
9185
+ },
9186
+ MachineAppliedInput: {
9187
+ type: "object",
9188
+ required: ["hostname"],
9189
+ properties: {
9190
+ hostname: { type: "string" }
9191
+ }
9192
+ },
9193
+ FeedbackInput: {
9194
+ type: "object",
9195
+ required: ["message"],
9196
+ properties: {
9197
+ message: { type: "string" },
9198
+ email: { type: "string" },
9199
+ category: { type: "string" },
9200
+ version: { type: "string" }
9201
+ }
9202
+ },
8759
9203
  AddProfileConfigInput: {
8760
9204
  type: "object",
8761
9205
  required: ["config_id"],
@@ -8890,7 +9334,111 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
8890
9334
  type: "object",
8891
9335
  required: ["items", "total", "limit", "cursor", "next_cursor", "has_more", "complete", "truncated", "source_bounded"],
8892
9336
  properties: {
9337
+ configs: { type: "array", items: { $ref: "#/components/schemas/Config" } },
8893
9338
  items: { type: "array", items: { $ref: "#/components/schemas/Config" } },
9339
+ count: { type: "number" },
9340
+ total: { type: "number" },
9341
+ limit: { type: "number" },
9342
+ cursor: { type: "number" },
9343
+ next_cursor: { type: "number", nullable: true },
9344
+ has_more: { type: "boolean" },
9345
+ complete: { type: "boolean" },
9346
+ truncated: { type: "boolean", const: false },
9347
+ source_bounded: { type: "boolean" }
9348
+ }
9349
+ },
9350
+ BoundedConfigIdentityPage: {
9351
+ type: "object",
9352
+ required: ["items", "total", "limit", "cursor", "next_cursor", "has_more", "complete", "truncated", "source_bounded"],
9353
+ properties: {
9354
+ configs: { type: "array", items: { $ref: "#/components/schemas/ConfigIdentity" } },
9355
+ items: { type: "array", items: { $ref: "#/components/schemas/ConfigIdentity" } },
9356
+ count: { type: "number" },
9357
+ total: { type: "number" },
9358
+ limit: { type: "number" },
9359
+ cursor: { type: "number" },
9360
+ next_cursor: { type: "number", nullable: true },
9361
+ has_more: { type: "boolean" },
9362
+ complete: { type: "boolean" },
9363
+ truncated: { type: "boolean", const: false },
9364
+ source_bounded: { type: "boolean" }
9365
+ }
9366
+ },
9367
+ BoundedProfileIdentityPage: {
9368
+ type: "object",
9369
+ required: ["items", "total", "limit", "cursor", "next_cursor", "has_more", "complete", "truncated", "source_bounded"],
9370
+ properties: {
9371
+ profiles: { type: "array", items: { $ref: "#/components/schemas/ProfileIdentity" } },
9372
+ items: { type: "array", items: { $ref: "#/components/schemas/ProfileIdentity" } },
9373
+ count: { type: "number" },
9374
+ total: { type: "number" },
9375
+ limit: { type: "number" },
9376
+ cursor: { type: "number" },
9377
+ next_cursor: { type: "number", nullable: true },
9378
+ has_more: { type: "boolean" },
9379
+ complete: { type: "boolean" },
9380
+ truncated: { type: "boolean", const: false },
9381
+ source_bounded: { type: "boolean" }
9382
+ }
9383
+ },
9384
+ BoundedSnapshotPage: {
9385
+ type: "object",
9386
+ required: ["items", "total", "limit", "cursor", "next_cursor", "has_more", "complete", "truncated", "source_bounded"],
9387
+ properties: {
9388
+ snapshots: { type: "array", items: { $ref: "#/components/schemas/ConfigSnapshot" } },
9389
+ items: { type: "array", items: { $ref: "#/components/schemas/ConfigSnapshot" } },
9390
+ count: { type: "number" },
9391
+ total: { type: "number" },
9392
+ limit: { type: "number" },
9393
+ cursor: { type: "number" },
9394
+ next_cursor: { type: "number", nullable: true },
9395
+ has_more: { type: "boolean" },
9396
+ complete: { type: "boolean" },
9397
+ truncated: { type: "boolean", const: false },
9398
+ source_bounded: { type: "boolean" }
9399
+ }
9400
+ },
9401
+ BoundedMachinePage: {
9402
+ type: "object",
9403
+ required: ["items", "total", "limit", "cursor", "next_cursor", "has_more", "complete", "truncated", "source_bounded"],
9404
+ properties: {
9405
+ machines: { type: "array", items: { $ref: "#/components/schemas/Machine" } },
9406
+ items: { type: "array", items: { $ref: "#/components/schemas/Machine" } },
9407
+ count: { type: "number" },
9408
+ total: { type: "number" },
9409
+ limit: { type: "number" },
9410
+ cursor: { type: "number" },
9411
+ next_cursor: { type: "number", nullable: true },
9412
+ has_more: { type: "boolean" },
9413
+ complete: { type: "boolean" },
9414
+ truncated: { type: "boolean", const: false },
9415
+ source_bounded: { type: "boolean" }
9416
+ }
9417
+ },
9418
+ BoundedProfileConfigBindingPage: {
9419
+ type: "object",
9420
+ required: ["items", "total", "limit", "cursor", "next_cursor", "has_more", "complete", "truncated", "source_bounded"],
9421
+ properties: {
9422
+ bindings: { type: "array", items: { $ref: "#/components/schemas/ProfileConfigBinding" } },
9423
+ items: { type: "array", items: { $ref: "#/components/schemas/ProfileConfigBinding" } },
9424
+ count: { type: "number" },
9425
+ total: { type: "number" },
9426
+ limit: { type: "number" },
9427
+ cursor: { type: "number" },
9428
+ next_cursor: { type: "number", nullable: true },
9429
+ has_more: { type: "boolean" },
9430
+ complete: { type: "boolean" },
9431
+ truncated: { type: "boolean", const: false },
9432
+ source_bounded: { type: "boolean" }
9433
+ }
9434
+ },
9435
+ BoundedProfileAssetBindingPage: {
9436
+ type: "object",
9437
+ required: ["items", "total", "limit", "cursor", "next_cursor", "has_more", "complete", "truncated", "source_bounded"],
9438
+ properties: {
9439
+ assets: { type: "array", items: { $ref: "#/components/schemas/ProfileAssetBinding" } },
9440
+ items: { type: "array", items: { $ref: "#/components/schemas/ProfileAssetBinding" } },
9441
+ count: { type: "number" },
8894
9442
  total: { type: "number" },
8895
9443
  limit: { type: "number" },
8896
9444
  cursor: { type: "number" },
@@ -8934,18 +9482,20 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
8934
9482
  { name: "category", in: "query", schema: { type: "string" } },
8935
9483
  { name: "agent", in: "query", schema: { type: "string" } },
8936
9484
  { name: "kind", in: "query", schema: { type: "string" } },
8937
- { name: "search", in: "query", schema: { type: "string" } }
9485
+ { name: "search", in: "query", schema: { type: "string", maxLength: 512 } },
9486
+ limitParameter,
9487
+ cursorParameter,
9488
+ identityViewParameter
8938
9489
  ],
8939
9490
  responses: {
8940
9491
  "200": {
8941
9492
  content: {
8942
9493
  "application/json": {
8943
9494
  schema: {
8944
- type: "object",
8945
- properties: {
8946
- configs: { type: "array", items: { $ref: "#/components/schemas/Config" } },
8947
- count: { type: "number" }
8948
- }
9495
+ oneOf: [
9496
+ { $ref: "#/components/schemas/BoundedConfigPage" },
9497
+ { $ref: "#/components/schemas/BoundedConfigIdentityPage" }
9498
+ ]
8949
9499
  }
8950
9500
  }
8951
9501
  }
@@ -8955,6 +9505,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
8955
9505
  post: {
8956
9506
  operationId: "createConfig",
8957
9507
  summary: "Create a config",
9508
+ parameters: [idempotencyKeyParameter],
8958
9509
  requestBody: {
8959
9510
  required: true,
8960
9511
  content: { "application/json": { schema: { $ref: "#/components/schemas/CreateConfigInput" } } }
@@ -8966,7 +9517,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
8966
9517
  schema: { type: "object", properties: { config: { $ref: "#/components/schemas/Config" } } }
8967
9518
  }
8968
9519
  }
8969
- }
9520
+ },
9521
+ "409": idempotencyConflictResponse
8970
9522
  }
8971
9523
  }
8972
9524
  },
@@ -9003,6 +9555,24 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
9003
9555
  }
9004
9556
  }
9005
9557
  },
9558
+ put: {
9559
+ operationId: "putConfig",
9560
+ summary: "Update a config via PUT",
9561
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
9562
+ requestBody: {
9563
+ required: true,
9564
+ content: { "application/json": { schema: { $ref: "#/components/schemas/UpdateConfigInput" } } }
9565
+ },
9566
+ responses: {
9567
+ "200": {
9568
+ content: {
9569
+ "application/json": {
9570
+ schema: { type: "object", properties: { config: { $ref: "#/components/schemas/Config" } } }
9571
+ }
9572
+ }
9573
+ }
9574
+ }
9575
+ },
9006
9576
  delete: {
9007
9577
  operationId: "deleteConfig",
9008
9578
  summary: "Delete a config",
@@ -9022,29 +9592,72 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
9022
9592
  get: {
9023
9593
  operationId: "listSnapshots",
9024
9594
  summary: "List a config's version snapshots",
9025
- parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
9026
- responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { snapshots: { type: "array", items: { type: "object" } }, count: { type: "number" } } } } } } }
9595
+ parameters: [
9596
+ { name: "id", in: "path", required: true, schema: { type: "string" } },
9597
+ limitParameter,
9598
+ cursorParameter
9599
+ ],
9600
+ responses: { "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/BoundedSnapshotPage" } } } } }
9027
9601
  },
9028
9602
  post: {
9029
9603
  operationId: "createSnapshot",
9030
9604
  summary: "Snapshot a config's current content",
9605
+ parameters: [
9606
+ { name: "id", in: "path", required: true, schema: { type: "string" } },
9607
+ idempotencyKeyParameter
9608
+ ],
9609
+ requestBody: {
9610
+ required: false,
9611
+ content: { "application/json": { schema: { type: "object", properties: { content: { type: "string" }, version: { type: "integer" } } } } }
9612
+ },
9613
+ responses: {
9614
+ "201": { content: { "application/json": { schema: { type: "object", properties: { snapshot: { $ref: "#/components/schemas/ConfigSnapshot" } } } } } },
9615
+ "409": idempotencyConflictResponse
9616
+ }
9617
+ }
9618
+ },
9619
+ "/v1/configs/{id}/snapshots/prune": {
9620
+ post: {
9621
+ operationId: "pruneSnapshots",
9622
+ summary: "Prune older snapshots for a config",
9031
9623
  parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
9032
- responses: { "201": { content: { "application/json": { schema: { type: "object", properties: { snapshot: { type: "object" } } } } } } }
9624
+ requestBody: {
9625
+ required: false,
9626
+ content: { "application/json": { schema: { $ref: "#/components/schemas/PruneSnapshotsInput" } } }
9627
+ },
9628
+ responses: {
9629
+ "200": { content: { "application/json": { schema: { type: "object", required: ["pruned"], properties: { pruned: { type: "integer" } } } } } }
9630
+ }
9631
+ }
9632
+ },
9633
+ "/v1/configs/{id}/snapshots/{version}": {
9634
+ get: {
9635
+ operationId: "getSnapshotByVersion",
9636
+ summary: "Get one config snapshot by version",
9637
+ parameters: [
9638
+ { name: "id", in: "path", required: true, schema: { type: "string" } },
9639
+ { name: "version", in: "path", required: true, schema: { type: "integer" } }
9640
+ ],
9641
+ responses: {
9642
+ "200": { content: { "application/json": { schema: { type: "object", properties: { snapshot: { $ref: "#/components/schemas/ConfigSnapshot" } } } } } }
9643
+ }
9033
9644
  }
9034
9645
  },
9035
9646
  "/v1/profiles": {
9036
9647
  get: {
9037
9648
  operationId: "listProfiles",
9038
9649
  summary: "List profiles with producer-side bounds",
9039
- parameters: [
9040
- { name: "limit", in: "query", schema: { type: "integer", minimum: 1, maximum: 100 } },
9041
- { name: "cursor", in: "query", schema: { type: "integer", minimum: 0 } }
9042
- ],
9650
+ parameters: [limitParameter, cursorParameter, identityViewParameter],
9043
9651
  responses: {
9044
9652
  "200": {
9045
9653
  content: {
9046
9654
  "application/json": {
9047
- schema: { $ref: "#/components/schemas/BoundedProfilePage" }
9655
+ schema: {
9656
+ oneOf: [
9657
+ { $ref: "#/components/schemas/BoundedProfilePage" },
9658
+ { $ref: "#/components/schemas/BoundedProfileIdentityPage" }
9659
+ ]
9660
+ }
9048
9661
  }
9049
9662
  }
9050
9663
  }
@@ -9053,11 +9666,15 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
9053
9666
  post: {
9054
9667
  operationId: "createProfile",
9055
9668
  summary: "Create a profile",
9669
+ parameters: [idempotencyKeyParameter],
9056
9670
  requestBody: {
9057
9671
  required: true,
9058
9672
  content: { "application/json": { schema: { $ref: "#/components/schemas/CreateProfileInput" } } }
9059
9673
  },
9060
- responses: { "201": { content: { "application/json": { schema: { type: "object", properties: { profile: { $ref: "#/components/schemas/Profile" } } } } } } }
9674
+ responses: {
9675
+ "201": { content: { "application/json": { schema: { type: "object", properties: { profile: { $ref: "#/components/schemas/Profile" } } } } } },
9676
+ "409": idempotencyConflictResponse
9677
+ }
9061
9678
  }
9062
9679
  },
9063
9680
  "/v1/profiles/resolve": {
@@ -9092,6 +9709,31 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
9092
9709
  ],
9093
9710
  responses: { "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/ProfileShowResponse" } } } } }
9094
9711
  },
9712
+ patch: {
9713
+ operationId: "updateProfile",
9714
+ summary: "Update a profile",
9715
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
9716
+ requestBody: {
9717
+ required: true,
9718
+ content: { "application/json": { schema: { $ref: "#/components/schemas/UpdateProfileInput" } } }
9719
+ },
9720
+ responses: {
9721
+ "200": { content: { "application/json": { schema: { type: "object", properties: { profile: { $ref: "#/components/schemas/Profile" } } } } } }
9722
+ }
9723
+ },
9724
+ put: {
9725
+ operationId: "putProfile",
9726
+ summary: "Update a profile via PUT",
9727
+ description: "Compatibility update route. Like PATCH, omitted properties remain unchanged.",
9728
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
9729
+ requestBody: {
9730
+ required: true,
9731
+ content: { "application/json": { schema: { $ref: "#/components/schemas/UpdateProfileInput" } } }
9732
+ },
9733
+ responses: {
9734
+ "200": { content: { "application/json": { schema: { type: "object", properties: { profile: { $ref: "#/components/schemas/Profile" } } } } } }
9735
+ }
9736
+ },
9095
9737
  delete: {
9096
9738
  operationId: "deleteProfile",
9097
9739
  summary: "Delete a profile",
@@ -9106,7 +9748,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
9106
9748
  description: "Requires an API key with the `instructions:write` scope.",
9107
9749
  security: [{ apiKey: [] }],
9108
9750
  parameters: [
9109
- { name: "id", in: "path", required: true, schema: { type: "string" } }
9751
+ { name: "id", in: "path", required: true, schema: { type: "string" } },
9752
+ idempotencyKeyParameter
9110
9753
  ],
9111
9754
  requestBody: {
9112
9755
  required: true,
@@ -9123,7 +9766,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
9123
9766
  schema: { $ref: "#/components/schemas/ProfileConfigAddedResponse" }
9124
9767
  }
9125
9768
  }
9126
- }
9769
+ },
9770
+ "409": idempotencyConflictResponse
9127
9771
  }
9128
9772
  }
9129
9773
  },
@@ -9134,13 +9778,17 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
9134
9778
  security: [{ apiKey: [] }],
9135
9779
  parameters: [
9136
9780
  { name: "id", in: "path", required: true, schema: { type: "string" } },
9137
- { name: "configId", in: "path", required: true, schema: { type: "string" } }
9781
+ { name: "configId", in: "path", required: true, schema: { type: "string" } },
9782
+ idempotencyKeyParameter
9138
9783
  ],
9139
9784
  requestBody: {
9140
9785
  required: true,
9141
9786
  content: { "application/json": { schema: { type: "object", required: ["binding"], properties: { binding: { $ref: "#/components/schemas/ProfileConfigBindingSpec" } } } } }
9142
9787
  },
9143
- responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { binding: { $ref: "#/components/schemas/ProfileConfigBinding" } } } } } } }
9788
+ responses: {
9789
+ "200": { content: { "application/json": { schema: { type: "object", properties: { binding: { $ref: "#/components/schemas/ProfileConfigBinding" } } } } } },
9790
+ "409": idempotencyConflictResponse
9791
+ }
9144
9792
  },
9145
9793
  delete: {
9146
9794
  operationId: "removeConfigFromProfile",
@@ -9166,24 +9814,38 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
9166
9814
  get: {
9167
9815
  operationId: "getProfileConfigBindings",
9168
9816
  summary: "List schema-versioned config bindings for a profile",
9169
- parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
9170
- responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { bindings: { type: "array", items: { $ref: "#/components/schemas/ProfileConfigBinding" } } } } } } } }
9817
+ parameters: [
9818
+ { name: "id", in: "path", required: true, schema: { type: "string" } },
9819
+ limitParameter,
9820
+ cursorParameter
9821
+ ],
9822
+ responses: { "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/BoundedProfileConfigBindingPage" } } } } }
9171
9823
  }
9172
9824
  },
9173
9825
  "/v1/profiles/{id}/assets": {
9174
9826
  get: {
9175
9827
  operationId: "getProfileAssetBindings",
9176
9828
  summary: "List typed asset bindings for a profile",
9177
- parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
9178
- responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { assets: { type: "array", items: { $ref: "#/components/schemas/ProfileAssetBinding" } } } } } } } }
9829
+ parameters: [
9830
+ { name: "id", in: "path", required: true, schema: { type: "string" } },
9831
+ limitParameter,
9832
+ cursorParameter
9833
+ ],
9834
+ responses: { "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/BoundedProfileAssetBindingPage" } } } } }
9179
9835
  },
9180
9836
  post: {
9181
9837
  operationId: "addAssetToProfile",
9182
9838
  summary: "Add a content-addressed asset binding to a profile",
9183
9839
  security: [{ apiKey: [] }],
9184
- parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
9840
+ parameters: [
9841
+ { name: "id", in: "path", required: true, schema: { type: "string" } },
9842
+ idempotencyKeyParameter
9843
+ ],
9185
9844
  requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/AddProfileAssetInput" } } } },
9186
- responses: { "201": { content: { "application/json": { schema: { type: "object", properties: { asset: { $ref: "#/components/schemas/ProfileAssetBinding" } } } } } } }
9845
+ responses: {
9846
+ "201": { content: { "application/json": { schema: { type: "object", properties: { asset: { $ref: "#/components/schemas/ProfileAssetBinding" } } } } } },
9847
+ "409": idempotencyConflictResponse
9848
+ }
9187
9849
  }
9188
9850
  },
9189
9851
  "/v1/profiles/{id}/assets/{assetKey}": {
@@ -9193,10 +9855,14 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
9193
9855
  security: [{ apiKey: [] }],
9194
9856
  parameters: [
9195
9857
  { name: "id", in: "path", required: true, schema: { type: "string" } },
9196
- { name: "assetKey", in: "path", required: true, schema: { type: "string" } }
9858
+ { name: "assetKey", in: "path", required: true, schema: { type: "string" } },
9859
+ idempotencyKeyParameter
9197
9860
  ],
9198
9861
  requestBody: { required: true, content: { "application/json": { schema: { type: "object", required: ["binding"], properties: { binding: { $ref: "#/components/schemas/ProfileAssetBindingSpec" } } } } } },
9199
- responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { asset: { $ref: "#/components/schemas/ProfileAssetBinding" } } } } } } }
9862
+ responses: {
9863
+ "200": { content: { "application/json": { schema: { type: "object", properties: { asset: { $ref: "#/components/schemas/ProfileAssetBinding" } } } } } },
9864
+ "409": idempotencyConflictResponse
9865
+ }
9200
9866
  },
9201
9867
  delete: {
9202
9868
  operationId: "removeAssetFromProfile",
@@ -9209,6 +9875,77 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
9209
9875
  responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { removed: { type: "boolean", const: true } } } } } } }
9210
9876
  }
9211
9877
  },
9878
+ "/v1/snapshots/{id}": {
9879
+ get: {
9880
+ operationId: "getSnapshot",
9881
+ summary: "Get a snapshot by id",
9882
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
9883
+ responses: {
9884
+ "200": { content: { "application/json": { schema: { type: "object", properties: { snapshot: { $ref: "#/components/schemas/ConfigSnapshot" } } } } } }
9885
+ }
9886
+ }
9887
+ },
9888
+ "/v1/machines/applied": {
9889
+ post: {
9890
+ operationId: "markMachineApplied",
9891
+ summary: "Mark a machine as having applied its resolved instructions",
9892
+ requestBody: {
9893
+ required: true,
9894
+ content: { "application/json": { schema: { $ref: "#/components/schemas/MachineAppliedInput" } } }
9895
+ },
9896
+ responses: {
9897
+ "200": { content: { "application/json": { schema: { type: "object", required: ["updated"], properties: { updated: { type: "boolean", const: true } } } } } }
9898
+ }
9899
+ }
9900
+ },
9901
+ "/v1/machines": {
9902
+ get: {
9903
+ operationId: "listMachines",
9904
+ summary: "List registered machines with producer-side bounds",
9905
+ parameters: [limitParameter, cursorParameter, identityViewParameter],
9906
+ responses: {
9907
+ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/BoundedMachinePage" } } } }
9908
+ }
9909
+ },
9910
+ post: {
9911
+ operationId: "registerMachine",
9912
+ summary: "Register or refresh a machine",
9913
+ parameters: [idempotencyKeyParameter],
9914
+ requestBody: {
9915
+ required: true,
9916
+ content: {
9917
+ "application/json": {
9918
+ schema: {
9919
+ type: "object",
9920
+ required: ["hostname"],
9921
+ properties: {
9922
+ hostname: { type: "string" },
9923
+ os: { type: "string", nullable: true },
9924
+ arch: { type: "string", nullable: true }
9925
+ }
9926
+ }
9927
+ }
9928
+ }
9929
+ },
9930
+ responses: {
9931
+ "201": { content: { "application/json": { schema: { type: "object", properties: { machine: { $ref: "#/components/schemas/Machine" } } } } } },
9932
+ "409": idempotencyConflictResponse
9933
+ }
9934
+ }
9935
+ },
9936
+ "/v1/feedback": {
9937
+ post: {
9938
+ operationId: "createFeedback",
9939
+ summary: "Submit Instructions feedback",
9940
+ requestBody: {
9941
+ required: true,
9942
+ content: { "application/json": { schema: { $ref: "#/components/schemas/FeedbackInput" } } }
9943
+ },
9944
+ responses: {
9945
+ "201": { content: { "application/json": { schema: { type: "object", required: ["ok"], properties: { ok: { type: "boolean", const: true } } } } } }
9946
+ }
9947
+ }
9948
+ },
9212
9949
  "/v1/stats": {
9213
9950
  get: {
9214
9951
  operationId: "getStats",
@@ -9241,7 +9978,7 @@ if (process.argv.includes("--version") || process.argv.includes("-V")) {
9241
9978
  if (process.argv.includes("--help") || process.argv.includes("-h")) {
9242
9979
  console.log(`Usage: instructions-serve [options]
9243
9980
 
9244
- HTTP API server for @hasna/instructions (backend: sqlite or postgresql).
9981
+ HTTP API server for @hasna/instructions (PostgreSQL is required for /v1).
9245
9982
 
9246
9983
  Options:
9247
9984
  -V, --version output the version number
@@ -9252,26 +9989,28 @@ Environment:
9252
9989
  PORT / INSTRUCTIONS_PORT HTTP port (default: 3457)
9253
9990
  HOST / INSTRUCTIONS_HOST bind host (default: localhost)
9254
9991
  HASNA_INSTRUCTIONS_DATABASE_URL / INSTRUCTIONS_DATABASE_URL / DATABASE_URL
9255
- postgresql backend URL (enables the cloud backend)`);
9992
+ PostgreSQL backend URL (required for /v1)`);
9256
9993
  process.exit(0);
9257
9994
  }
9258
9995
  var PORT = Number(process.env["PORT"] ?? process.env["INSTRUCTIONS_PORT"] ?? 3457);
9259
9996
  var app = new Hono2;
9260
9997
  app.use("*", cors());
9261
9998
  function serviceBackend() {
9262
- return isPostgresBackendEnabled() ? "postgresql" : "sqlite";
9999
+ return isPostgresBackendEnabled() ? "postgresql" : "unconfigured";
9263
10000
  }
9264
10001
  app.get("/health", (c) => c.json({ status: "ok", version: getPackageVersion(), backend: serviceBackend(), name: "instructions" }));
9265
10002
  app.get("/version", (c) => c.json({ status: "ok", version: getPackageVersion(), backend: serviceBackend(), name: "instructions" }));
9266
10003
  app.get("/ready", async (c) => {
9267
10004
  const version = getPackageVersion();
9268
10005
  const backend2 = serviceBackend();
9269
- if (backend2 === "postgresql") {
9270
- try {
9271
- await pingCloud();
9272
- } catch (e) {
9273
- return c.json({ status: "unavailable", version, backend: backend2, error: e.message }, 503);
9274
- }
10006
+ if (backend2 !== "postgresql") {
10007
+ return c.json({ status: "unavailable", version, backend: backend2, code: "SERVER_BACKEND_UNCONFIGURED" }, 503);
10008
+ }
10009
+ try {
10010
+ await pingCloud();
10011
+ } catch {
10012
+ console.error("instructions ready: database unavailable");
10013
+ return c.json({ status: "unavailable", version, backend: backend2, code: "DATABASE_UNAVAILABLE" }, 503);
9275
10014
  }
9276
10015
  return c.json({ status: "ready", version, backend: backend2 });
9277
10016
  });
@@ -9282,18 +10021,27 @@ app.use("/v1/*", async (c, next) => {
9282
10021
  let mw;
9283
10022
  try {
9284
10023
  mw = getHonoAuthMiddleware([isWrite ? "instructions:write" : "instructions:read"]);
9285
- } catch (e) {
9286
- return c.json({ error: e.message }, 503);
10024
+ } catch {
10025
+ return c.json({ error: "Instructions API is unavailable", code: "API_AUTH_UNCONFIGURED" }, 503);
9287
10026
  }
9288
10027
  return mw(c, next);
9289
10028
  });
9290
10029
  app.all("/v1/*", async (c) => {
9291
- const res = await handleV1Request(c.req.raw, new URL(c.req.url));
10030
+ const res = await handleV1Request(c.req.raw, new URL(c.req.url), {
10031
+ principal: c.get("apiKey")
10032
+ });
9292
10033
  return res ?? c.json({ error: "Not found" }, 404);
9293
10034
  });
9294
10035
  var HOST = process.env["HOST"] ?? process.env["INSTRUCTIONS_HOST"] ?? "localhost";
9295
- console.log(`instructions-serve listening on http://${HOST}:${PORT} (backend: ${serviceBackend()})`);
9296
- var server_default = { port: PORT, hostname: HOST, fetch: app.fetch };
10036
+ var serverOptions = { port: PORT, hostname: HOST, fetch: app.fetch };
10037
+ if (import.meta.main) {
10038
+ console.log(`instructions-serve listening on http://${HOST}:${PORT} (backend: ${serviceBackend()})`);
10039
+ Bun.serve(serverOptions);
10040
+ }
10041
+ var server_default = app;
9297
10042
  export {
9298
- server_default as default
10043
+ serviceBackend,
10044
+ serverOptions,
10045
+ server_default as default,
10046
+ app
9299
10047
  };