makersclaw 0.6.0 → 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 (141) hide show
  1. package/README.md +48 -8
  2. package/package.json +1 -1
  3. package/r/annotate.json +4 -4
  4. package/r/approval-request.json +3 -3
  5. package/r/audio-player.json +3 -3
  6. package/r/auth-form.json +2 -2
  7. package/r/board.json +4 -4
  8. package/r/calendar-view.json +4 -4
  9. package/r/cart.json +3 -3
  10. package/r/chart.json +3 -3
  11. package/r/chat.json +3 -3
  12. package/r/code-editor.json +4 -4
  13. package/r/comment-box.json +4 -4
  14. package/r/contact-section.json +3 -3
  15. package/r/core.json +3 -3
  16. package/r/csv-import.json +4 -4
  17. package/r/data-table.json +3 -3
  18. package/r/diagram.json +3 -3
  19. package/r/doc-editor.json +4 -4
  20. package/r/doc-page.json +4 -4
  21. package/r/email-render.json +2 -2
  22. package/r/faq.json +2 -2
  23. package/r/file-upload.json +3 -3
  24. package/r/flow-canvas.json +4 -4
  25. package/r/footer.json +3 -3
  26. package/r/gantt.json +4 -4
  27. package/r/job-panel.json +3 -3
  28. package/r/json-editor.json +4 -4
  29. package/r/map.json +5 -5
  30. package/r/menubar.json +2 -2
  31. package/r/mind-map.json +4 -4
  32. package/r/navigation-menu.json +2 -2
  33. package/r/order-detail.json +2 -2
  34. package/r/org-chart.json +4 -4
  35. package/r/page-builder.json +6 -6
  36. package/r/payment-methods.json +2 -2
  37. package/r/pdf-document.json +2 -2
  38. package/r/pdf-viewer.json +6 -6
  39. package/r/product-detail.json +3 -3
  40. package/r/product-grid.json +4 -4
  41. package/r/record-form.json +3 -3
  42. package/r/record-notes.json +5 -5
  43. package/r/registry.json +121 -121
  44. package/r/search-dialog.json +3 -3
  45. package/r/settings-list.json +3 -3
  46. package/r/setup-checklist.json +3 -3
  47. package/r/share-dialog.json +3 -3
  48. package/r/sidebar.json +2 -2
  49. package/r/signature-pad.json +3 -3
  50. package/r/sketch-canvas.json +5 -5
  51. package/r/spreadsheet.json +3 -3
  52. package/r/stat-cards.json +4 -4
  53. package/r/task-list.json +4 -4
  54. package/r/task-model.json +3 -3
  55. package/r/testimonials.json +2 -2
  56. package/r/transaction-list.json +3 -3
  57. package/r/whiteboard.json +4 -4
  58. package/registry/blocks/annotate/README.md +14 -5
  59. package/registry/blocks/approval-request/README.md +15 -8
  60. package/registry/blocks/audio-player/README.md +17 -10
  61. package/registry/blocks/board/README.md +13 -9
  62. package/registry/blocks/board/board.jsx +1 -1
  63. package/registry/blocks/calendar-view/README.md +28 -4
  64. package/registry/blocks/cart/README.md +14 -10
  65. package/registry/blocks/chart/README.md +14 -7
  66. package/registry/blocks/chat/README.md +43 -13
  67. package/registry/blocks/code-editor/README.md +4 -0
  68. package/registry/blocks/code-editor/code-editor.jsx +1 -1
  69. package/registry/blocks/comment-box/README.md +20 -13
  70. package/registry/blocks/contact-section/README.md +7 -6
  71. package/registry/blocks/csv-import/README.md +34 -12
  72. package/registry/blocks/data-table/README.md +41 -8
  73. package/registry/blocks/diagram/diagram.jsx +1 -1
  74. package/registry/blocks/doc-editor/README.md +18 -6
  75. package/registry/blocks/doc-editor/doc-editor.jsx +1 -1
  76. package/registry/blocks/doc-page/README.md +16 -6
  77. package/registry/blocks/file-upload/README.md +7 -1
  78. package/registry/blocks/flow-canvas/README.md +16 -5
  79. package/registry/blocks/flow-canvas/flow-canvas.jsx +1 -1
  80. package/registry/blocks/footer/README.md +4 -2
  81. package/registry/blocks/gantt/README.md +18 -12
  82. package/registry/blocks/job-panel/README.md +1 -1
  83. package/registry/blocks/json-editor/README.md +16 -4
  84. package/registry/blocks/map/README.md +19 -3
  85. package/registry/blocks/map/map-view.jsx +1 -1
  86. package/registry/blocks/map/maplibre.js +1 -1
  87. package/registry/blocks/mind-map/README.md +16 -5
  88. package/registry/blocks/org-chart/README.md +10 -7
  89. package/registry/blocks/page-builder/README.md +13 -6
  90. package/registry/blocks/page-builder/page-builder.jsx +1 -1
  91. package/registry/blocks/page-builder/page-view.jsx +1 -1
  92. package/registry/blocks/page-builder/page.js +1 -1
  93. package/registry/blocks/pdf-viewer/README.md +22 -20
  94. package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1 -1
  95. package/registry/blocks/pdf-viewer/pdfjs.js +1 -1
  96. package/registry/blocks/pdf-viewer/ssr-shim.js +1 -1
  97. package/registry/blocks/product-detail/README.md +13 -7
  98. package/registry/blocks/product-detail/demo.jsx +1 -1
  99. package/registry/blocks/product-grid/README.md +12 -10
  100. package/registry/blocks/record-form/README.md +26 -9
  101. package/registry/blocks/record-notes/README.md +42 -30
  102. package/registry/blocks/search-dialog/README.md +38 -7
  103. package/registry/blocks/settings-list/README.md +29 -17
  104. package/registry/blocks/setup-checklist/README.md +12 -14
  105. package/registry/blocks/share-dialog/README.md +1 -1
  106. package/registry/blocks/signature-pad/README.md +15 -4
  107. package/registry/blocks/sketch-canvas/README.md +13 -4
  108. package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1 -1
  109. package/registry/blocks/sketch-canvas/ssr-shim.js +3 -3
  110. package/registry/blocks/spreadsheet/README.md +17 -8
  111. package/registry/blocks/stat-cards/README.md +17 -8
  112. package/registry/blocks/task-list/README.md +14 -11
  113. package/registry/blocks/task-model/README.md +3 -3
  114. package/registry/blocks/transaction-list/README.md +15 -7
  115. package/registry/blocks/whiteboard/README.md +19 -13
  116. package/src/cli.mjs +31 -4
  117. package/src/install.mjs +263 -21
  118. package/src/registry.mjs +78 -1
  119. package/templates/next/browser/app/auto-refresh.jsx +67 -0
  120. package/templates/next/browser/app/layout.jsx +94 -0
  121. package/templates/next/browser/app/page.jsx +60 -0
  122. package/templates/next/browser/app/s/[key]/page.jsx +95 -0
  123. package/templates/next/browser/lib/actions.js +16 -0
  124. package/templates/next/browser/lib/data.js +43 -0
  125. package/templates/next/files/README.md +810 -79
  126. package/templates/next/files/app/frame.js +128 -4
  127. package/templates/next/files/check/run.mjs +1438 -41
  128. package/templates/next/files/components/ui/item.jsx +2 -2
  129. package/templates/next/files/components/ui/notice.jsx +1 -1
  130. package/templates/next/files/components/ui/separator.jsx +1 -1
  131. package/templates/next/files/lib/actions.js +170 -51
  132. package/templates/next/files/lib/brain-server.js +96 -0
  133. package/templates/next/files/lib/brain.js +413 -0
  134. package/templates/next/files/lib/claw.js +11 -0
  135. package/templates/next/files/lib/data.js +119 -29
  136. package/templates/next/files/lib/db.js +97 -17
  137. package/templates/next/files/lib/door.js +680 -0
  138. package/templates/next/files/next.config.mjs +36 -2
  139. package/templates/next/files/surfaces.js +3 -2
  140. package/templates/next/template.json +9 -3
  141. package/templates/next/files/schema.sql +0 -22
@@ -17,7 +17,7 @@ import { Icon } from "./icon";
17
17
  // meta a count, a date, a Badge — muted, on the right
18
18
  // actions buttons on the far right (IconButton, Menu); they sit above
19
19
  // the row's target, so they click on their own
20
- // href a next/link row · `onClick` a button row · neither: static
20
+ // href a next/link row · `onClick` a button row · neither: a plain row
21
21
  // size default (56px) · sm (44px, for dense lists and rails) ·
22
22
  // lg (64px, roomier: a pipeline's runs, a list read at leisure)
23
23
  // variant plain (no fill) · outline (a bordered card) · muted (a filled panel)
@@ -58,7 +58,7 @@ const STACKED = {
58
58
 
59
59
  const TARGET = "outline-none after:absolute after:inset-0";
60
60
 
61
- // The row filled while its target (or a static row itself) is current.
61
+ // The row filled while its target (or a plain row itself) is current.
62
62
  const CURRENT =
63
63
  "has-[[data-item-target][aria-current]:not([aria-current=false])]:bg-accent " +
64
64
  "[&[aria-current]:not([aria-current=false])]:bg-accent";
@@ -2,7 +2,7 @@ import { cx } from "./cx";
2
2
  import { IconButton } from "./button";
3
3
  import { Icon } from "./icon";
4
4
 
5
- // Static parts: a card, a notice, a badge, a kicker, the empty state.
5
+ // Plain parts: a card, a notice, a badge, a kicker, the empty state.
6
6
  // Server-safe (no hooks), so a server surface can use them directly.
7
7
 
8
8
  const TONES = {
@@ -1,6 +1,6 @@
1
1
  import { cx } from "./cx";
2
2
 
3
- // Small static parts that shape a layout: a separator, a key cap, a box
3
+ // Small plain parts that shape a layout: a separator, a key cap, a box
4
4
  // that keeps its ratio. Server-safe (no hooks).
5
5
 
6
6
  /**
@@ -2,13 +2,22 @@
2
2
 
3
3
  import { headers } from "next/headers";
4
4
  import { revalidatePath } from "next/cache";
5
- import { ident, logActivity, query } from "./db";
5
+ import { ident, logActivity, query, requestMember, transaction } from "./db";
6
6
 
7
7
  // Every write the app makes. Callable from server and client components
8
8
  // alike (they are server actions). Each one goes through lib/db.js,
9
9
  // records an activity row, and re-renders the app. Table and column names
10
10
  // are quoted through ident(), which refuses anything that is not a plain
11
- // identifier. Returns { ok } or { ok: false, error }; never throws.
11
+ // identifier. Returns { ok } or { ok: false, error, code }; never throws.
12
+ // `code` is the one the platform's data door answers a browser app with
13
+ // (lib/door.js), so a surface branches on it the same way in both modes.
14
+ //
15
+ // Each write runs in its own transaction that first tells the database
16
+ // who is asking (transaction() in lib/db.js: claw.member_id and
17
+ // claw.member_email, local to it), so a trigger or an action function
18
+ // reads the member here as it does in a browser app, behind the platform's
19
+ // door. The activity line is written after the COMMIT, never inside it:
20
+ // a line that fails must not undo the write it describes.
12
21
  //
13
22
  // `surface` is optional: proxy.js stamps the current surface key on
14
23
  // the request, so a surface rarely needs to pass it.
@@ -20,13 +29,12 @@ import { ident, logActivity, query } from "./db";
20
29
  // words. Pass a detail when the surface can say what changed in plain
21
30
  // words; leave it out and the tab shows the action word alone.
22
31
 
32
+ /** The line's `by`, as the door writes it: the member's email, else
33
+ * their id, else unknown (a local run). */
23
34
  async function actor() {
24
- try {
25
- const email = (await headers()).get("x-claw-user-email");
26
- return email ? `user:${email}` : "user:unknown";
27
- } catch {
28
- return "user:unknown";
29
- }
35
+ const { id, email } = await requestMember();
36
+ if (email) return `user:${email}`;
37
+ return id ? `user:${id}` : "user:unknown";
30
38
  }
31
39
 
32
40
  async function surfaceOf(explicit) {
@@ -97,36 +105,88 @@ async function setClause(source, cols) {
97
105
  return cols.map((c, i) => `${ident(c)} = $${i + 2}`).join(", ") + (await stampFor(source, cols));
98
106
  }
99
107
 
100
- function fail(error) {
108
+ // Postgres's raise_exception: what a plpgsql `raise exception` says.
109
+ const REFUSED = "P0001";
110
+
111
+ // The data door's codes for Postgres's errors (claw's STATIC-SPEC §1,
112
+ // "Errors reach the Builder"). Up to 0.6.x a failure here had no code, so
113
+ // a surface that branched on the door's `code` in a browser app fell
114
+ // through to its generic path after `makersclaw mode server`.
115
+ const CODES = new Map([
116
+ [REFUSED, "action_refused"],
117
+ ["23505", "conflict"], // unique
118
+ ["23503", "conflict"], // foreign key
119
+ ["23502", "bad_request"], // not null
120
+ ["22P02", "bad_request"], // not a value of the column's type
121
+ ["22007", "bad_request"], // not a date or time
122
+ ["22008", "bad_request"], // a date or time out of range
123
+ ["57014", "too_slow"],
124
+ ["40001", "try_again"], // serialization
125
+ ["40P01", "try_again"], // deadlock
126
+ ["55P03", "try_again"], // lock not available
127
+ ]);
128
+ const SQLSTATE = /^[0-9A-Z]{5}$/;
129
+
130
+ /** A failure's code, as the door gives it. `missing` names the one error
131
+ * that means the statement's own table or function is not there: 42P01
132
+ * no_such_table for a table's write, 42883 no_such_action for an
133
+ * action. Postgres raises it with no `where` (the context an error from
134
+ * inside a function or a trigger carries); from inside, it is the
135
+ * code's fault, bad_request, as the rest of class 42. ident()'s refusal
136
+ * is bad_request; a connection that failed is failed. */
137
+ function codeOf(error, missing) {
138
+ const state = typeof error?.code === "string" && SQLSTATE.test(error.code) ? error.code : "";
139
+ if (!state) return /^invalid identifier: /.test(error?.message ?? "") ? "bad_request" : "failed";
140
+ if (missing?.[state] && !error.where) return missing[state];
141
+ if (CODES.has(state)) return CODES.get(state);
142
+ return state.startsWith("42") ? "bad_request" : "failed";
143
+ }
144
+
145
+ function fail(error, missing) {
101
146
  const message = error?.message ?? String(error);
102
- console.error(`[actions] write failed: ${message}`);
103
- return { ok: false, error: message };
147
+ const code = codeOf(error, missing);
148
+ // A trigger's own raise exception is a deliberate no, as an action's is
149
+ // (runAction): a warning, or the log tail sends the platform agent after it.
150
+ if (code === "action_refused") console.warn(`[actions] refused: ${message}`);
151
+ else console.error(`[actions] write failed: ${message}`);
152
+ return { ok: false, error: message, code };
104
153
  }
105
154
 
155
+ /** A write refused here, with the door's code for it. */
156
+ const refused = (error, code) => ({ ok: false, error, code });
157
+
158
+ /** What the door reads a table's write failing with, when the table is not there. */
159
+ const NO_TABLE = { "42P01": "no_such_table" };
160
+
106
161
  /** Change columns of one row. `patch` is { column: value }; updated_at
107
- * becomes now() when the table has one (above). */
108
- export async function updateRow({ source, idColumn = "id", id, patch, surface, action = "edited", detail, data }) {
162
+ * becomes now() when the table has one (above). `log: false` writes no
163
+ * activity line: for a save that is typing, not a decision (an autosave
164
+ * every few keystrokes would flood the Activity tab). */
165
+ export async function updateRow({ source, idColumn = "id", id, patch, surface, action = "edited", detail, data, log }) {
109
166
  try {
167
+ const table = ident(source);
168
+ const key = ident(idColumn);
110
169
  const cols = Object.keys(patch ?? {}).filter((c) => patch[c] !== undefined);
111
- if (!cols.length) return { ok: false, error: "nothing to change" };
170
+ if (!cols.length) return refused("nothing to change", "bad_request");
112
171
  const sets = await setClause(source, cols);
113
- const { rowCount } = await query(
114
- `UPDATE ${ident(source)} SET ${sets} WHERE ${ident(idColumn)} = $1`,
115
- [id, ...cols.map((c) => patch[c])],
172
+ const { rowCount } = await transaction((q) =>
173
+ q(`UPDATE ${table} SET ${sets} WHERE ${key} = $1`, [id, ...cols.map((c) => patch[c])]),
116
174
  );
117
- if (!rowCount) return { ok: false, error: "row not found" };
118
- await logActivity({
119
- action,
120
- surface: await surfaceOf(surface),
121
- row_ref: id,
122
- detail: detail ?? `${cols.join(", ")} changed`,
123
- data: data ?? patch,
124
- by: await actor(),
125
- });
175
+ if (!rowCount) return refused("row not found", "not_found");
176
+ if (log !== false) {
177
+ await logActivity({
178
+ action,
179
+ surface: await surfaceOf(surface),
180
+ row_ref: id,
181
+ detail: detail ?? `${cols.join(", ")} changed`,
182
+ data: data ?? patch,
183
+ by: await actor(),
184
+ });
185
+ }
126
186
  done();
127
187
  return { ok: true };
128
188
  } catch (error) {
129
- return fail(error);
189
+ return fail(error, NO_TABLE);
130
190
  }
131
191
  }
132
192
 
@@ -148,12 +208,10 @@ export async function moveRow({ source, idColumn = "id", id, stateColumn, to, su
148
208
  export async function insertRow({ source, idColumn = "id", values, surface, action = "added", detail }) {
149
209
  try {
150
210
  const cols = Object.keys(values ?? {}).filter((c) => values[c] !== undefined);
151
- if (!cols.length) return { ok: false, error: "nothing to add" };
211
+ if (!cols.length) return refused("nothing to add", "bad_request");
152
212
  const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
153
- const { rows } = await query(
154
- `INSERT INTO ${ident(source)} (${cols.map(ident).join(", ")}) VALUES (${placeholders}) RETURNING ${ident(idColumn)}`,
155
- cols.map((c) => values[c]),
156
- );
213
+ const text = `INSERT INTO ${ident(source)} (${cols.map(ident).join(", ")}) VALUES (${placeholders}) RETURNING ${ident(idColumn)}`;
214
+ const { rows } = await transaction((q) => q(text, cols.map((c) => values[c])));
157
215
  const id = rows[0]?.[idColumn];
158
216
  await logActivity({
159
217
  action,
@@ -166,14 +224,15 @@ export async function insertRow({ source, idColumn = "id", values, surface, acti
166
224
  done();
167
225
  return { ok: true, id };
168
226
  } catch (error) {
169
- return fail(error);
227
+ return fail(error, NO_TABLE);
170
228
  }
171
229
  }
172
230
 
173
231
  export async function deleteRow({ source, idColumn = "id", id, surface, detail }) {
174
232
  try {
175
- const { rowCount } = await query(`DELETE FROM ${ident(source)} WHERE ${ident(idColumn)} = $1`, [id]);
176
- if (!rowCount) return { ok: false, error: "row not found" };
233
+ const text = `DELETE FROM ${ident(source)} WHERE ${ident(idColumn)} = $1`;
234
+ const { rowCount } = await transaction((q) => q(text, [id]));
235
+ if (!rowCount) return refused("row not found", "not_found");
177
236
  await logActivity({
178
237
  action: "removed",
179
238
  surface: await surfaceOf(surface),
@@ -184,7 +243,7 @@ export async function deleteRow({ source, idColumn = "id", id, surface, detail }
184
243
  done();
185
244
  return { ok: true };
186
245
  } catch (error) {
187
- return fail(error);
246
+ return fail(error, NO_TABLE);
188
247
  }
189
248
  }
190
249
 
@@ -195,7 +254,8 @@ export async function deleteRow({ source, idColumn = "id", id, surface, detail }
195
254
  * has a plain `key text` column and Postgres rejects ON CONFLICT without a
196
255
  * unique index on it. JSON mode sets the key inside the jsonb `column` of
197
256
  * the row `id` in `source`. An update stamps updated_at as updateRow's
198
- * does; the insert leaves it to the column's default.
257
+ * does; the insert leaves it to the column's default. The UPDATE and the
258
+ * INSERT run in one transaction (up to 0.6.x they were two).
199
259
  */
200
260
  export async function saveSetting({
201
261
  mode = "table",
@@ -210,24 +270,21 @@ export async function saveSetting({
210
270
  surface,
211
271
  }) {
212
272
  try {
213
- if (!key) return { ok: false, error: "no key" };
273
+ if (!key) return refused("no key", "bad_request");
214
274
  if (mode === "json") {
275
+ const col = ident(column);
215
276
  const stamp = await stampFor(source, [column]);
216
- const { rowCount } = await query(
217
- `UPDATE ${ident(source)} SET ${ident(column)} = jsonb_set(COALESCE(${ident(column)}, '{}'::jsonb), $2::text[], $3::jsonb, true)${stamp} WHERE ${ident(idColumn)} = $1`,
218
- [id, [key], JSON.stringify(value ?? null)],
219
- );
220
- if (!rowCount) return { ok: false, error: "row not found" };
277
+ const text = `UPDATE ${ident(source)} SET ${col} = jsonb_set(COALESCE(${col}, '{}'::jsonb), $2::text[], $3::jsonb, true)${stamp} WHERE ${ident(idColumn)} = $1`;
278
+ const { rowCount } = await transaction((q) => q(text, [id, [key], JSON.stringify(value ?? null)]));
279
+ if (!rowCount) return refused("row not found", "not_found");
221
280
  } else {
222
281
  const stored = typeof value === "object" && value !== null ? JSON.stringify(value) : value;
282
+ const [table, keyCol, valueCol] = [ident(source), ident(keyColumn), ident(valueColumn)];
223
283
  const stamp = await stampFor(source, [valueColumn]);
224
- const { rowCount } = await query(
225
- `UPDATE ${ident(source)} SET ${ident(valueColumn)} = $2${stamp} WHERE ${ident(keyColumn)} = $1`,
226
- [key, stored],
227
- );
228
- if (!rowCount) {
229
- await query(`INSERT INTO ${ident(source)} (${ident(keyColumn)}, ${ident(valueColumn)}) VALUES ($1, $2)`, [key, stored]);
230
- }
284
+ await transaction(async (q) => {
285
+ const { rowCount } = await q(`UPDATE ${table} SET ${valueCol} = $2${stamp} WHERE ${keyCol} = $1`, [key, stored]);
286
+ if (!rowCount) await q(`INSERT INTO ${table} (${keyCol}, ${valueCol}) VALUES ($1, $2)`, [key, stored]);
287
+ });
231
288
  }
232
289
  await logActivity({
233
290
  action: "edited",
@@ -240,6 +297,68 @@ export async function saveSetting({
240
297
  done();
241
298
  return { ok: true };
242
299
  } catch (error) {
243
- return fail(error);
300
+ return fail(error, NO_TABLE);
301
+ }
302
+ }
303
+
304
+ // An action's name as the door takes it: one bare, lower-case name. The
305
+ // door calls only a function of the app's own schema, so a name with a
306
+ // schema (`app_x.fn`) or capitals (`CloseCycle`), which ident() quotes
307
+ // and Postgres would find, worked here and is no_such_action there.
308
+ // Refused here too, so a surface that works in one mode works in the
309
+ // other.
310
+ const ACTION_NAME = /^[a-z_][a-z0-9_]{0,62}$/;
311
+
312
+ /**
313
+ * Run an action: a function in the app's own schema, `(args jsonb)
314
+ * returns jsonb`, for any write of more than one row, or one that decides
315
+ * something. The screen calls it here; the app's agent runs the same
316
+ * function with `sql` (`select close_cycle($1::jsonb)`), so the two never
317
+ * drift. It runs inside transaction(), after the member is set, so it
318
+ * reads who is asking from `current_setting('claw.member_email', true)`.
319
+ * It says no with `raise exception '<sentence>'`: the sentence comes back
320
+ * as `error`, code action_refused, and the log says it was refused, not
321
+ * that the app failed. A name that is not one bare lower-case name, or no
322
+ * function of it that takes jsonb, is no_such_action, as at the door.
323
+ *
324
+ * The activity line is this call's: `action` defaults to the function's
325
+ * name, `row_ref` is `rowRef` or the result's `id`. The function never
326
+ * writes claw_app.activity itself. `log: false` writes no line (an
327
+ * autosave). `args` defaults to {}.
328
+ * → { ok: true, result } | { ok: false, error, code }
329
+ */
330
+ export async function runAction({ name, args, surface, action, detail, rowRef, log } = {}) {
331
+ if (typeof name !== "string" || !ACTION_NAME.test(name)) {
332
+ const error = `no action ${JSON.stringify(name ?? null)}: an action is named in lower case, a-z, 0-9 and _, with no schema (close_cycle)`;
333
+ console.error(`[actions] ${error}`);
334
+ return refused(error, "no_such_action");
335
+ }
336
+ try {
337
+ const text = `SELECT ${ident(name)}($1::jsonb) AS result`;
338
+ const json = JSON.stringify(args === undefined ? {} : args);
339
+ const { rows } = await transaction((q) => q(text, [json]));
340
+ const result = rows[0]?.result ?? null;
341
+ if (log !== false) {
342
+ const returned = result && typeof result === "object" && !Array.isArray(result) ? result.id : undefined;
343
+ await logActivity({
344
+ action: action || name,
345
+ surface: await surfaceOf(surface),
346
+ row_ref: rowRef ?? returned ?? null,
347
+ detail: detail ?? null,
348
+ by: await actor(),
349
+ });
350
+ }
351
+ done();
352
+ return { ok: true, result };
353
+ } catch (error) {
354
+ // P0001 is the function's own raise exception: a deliberate no ("that
355
+ // cycle is already closed"), which the member acts on. Logged as an
356
+ // error, the log tail would send the platform agent chasing it as a
357
+ // bug, which the door avoids by not recording it either.
358
+ if (error?.code === REFUSED) {
359
+ console.warn(`[actions] ${name} refused: ${error.message}`);
360
+ return refused(error.message, "action_refused");
361
+ }
362
+ return fail(error, { "42883": "no_such_action" });
244
363
  }
245
364
  }
@@ -0,0 +1,96 @@
1
+ import { headers } from "next/headers";
2
+ import { clawAnswer } from "./claw";
3
+ import { brainRead } from "./brain";
4
+
5
+ // The company brain's reads, from a SERVER component, in a server app.
6
+ // The same nine reads as lib/brain.js, with the same arguments and
7
+ // results (claw docs/brain/CONTRACTS.md §10); no writes. A write from the
8
+ // server would be app code acting, never the member's press, so writes go
9
+ // from the browser through lib/brain.js even here.
10
+ //
11
+ // The browser's door grant is its cookie, which never reaches this
12
+ // server. Here the grant is the bearer the product proxy stamps on each
13
+ // member's request as `x-claw-door` (5 minutes, bound to this app and
14
+ // that member), read with headers(), and the door is CLAW_DOOR_URL (the
15
+ // app's public origin and path plus /_claw/data). No header (a render
16
+ // outside a member's request, or a machine whose proxy does not stamp it
17
+ // yet) or no CLAW_DOOR_URL: each read answers { ok: false, unavailable:
18
+ // true, code: "no_door" } and asks nothing. Read in the browser then.
19
+ //
20
+ // This file pulls in next/headers, so a "use client" file cannot import
21
+ // it (the build says so), as with lib/data.js.
22
+
23
+ const NO_DOOR = "no door grant on this request: read the brain in the browser (lib/brain.js)";
24
+
25
+ /** The bearer the proxy stamped on this request, or null. */
26
+ async function doorGrant() {
27
+ try {
28
+ return (await headers()).get("x-claw-door") || null;
29
+ } catch {
30
+ return null; // outside a request there are no headers at all
31
+ }
32
+ }
33
+
34
+ /** Send one read to the door with the bearer: a claw()-shaped answer. */
35
+ async function viaDoorGrant(path, signal) {
36
+ const grant = await doorGrant();
37
+ const door = String(process.env.CLAW_DOOR_URL ?? "").replace(/\/+$/, "");
38
+ if (!grant || !door) return { ok: false, status: 0, error: NO_DOOR, code: "no_door", unavailable: true };
39
+ let res;
40
+ try {
41
+ res = await fetch(`${door}/${path}`, {
42
+ cache: "no-store",
43
+ headers: { accept: "application/json", authorization: `Bearer ${grant}` },
44
+ signal,
45
+ });
46
+ } catch (error) {
47
+ if (error?.name === "AbortError") return { ok: false, status: 0, error: "aborted", aborted: true };
48
+ return { ok: false, status: 0, error: "not reachable", unavailable: true };
49
+ }
50
+ return clawAnswer(res);
51
+ }
52
+
53
+ /** → { ok, brain, kinds, error }, as lib/brain.js kinds(). */
54
+ export function kinds(args) {
55
+ return brainRead("kinds", args, viaDoorGrant);
56
+ }
57
+
58
+ /** → { ok, records, next, error }, as lib/brain.js listRecords(). */
59
+ export function listRecords(args) {
60
+ return brainRead("listRecords", args, viaDoorGrant);
61
+ }
62
+
63
+ /** → { ok, record, links, error }, as lib/brain.js getRecord(). */
64
+ export function getRecord(args) {
65
+ return brainRead("getRecord", args, viaDoorGrant);
66
+ }
67
+
68
+ /** → { ok, count, by, error }, as lib/brain.js countRecords(). */
69
+ export function countRecords(args) {
70
+ return brainRead("countRecords", args, viaDoorGrant);
71
+ }
72
+
73
+ /** → { ok, meaning, results, error }, as lib/brain.js searchBrain(). */
74
+ export function searchBrain(args) {
75
+ return brainRead("searchBrain", args, viaDoorGrant);
76
+ }
77
+
78
+ /** → { ok, changes, next, error }, as lib/brain.js recordHistory(). */
79
+ export function recordHistory(args) {
80
+ return brainRead("recordHistory", args, viaDoorGrant);
81
+ }
82
+
83
+ /** → { ok, changes, next, error }, as lib/brain.js brainChanges(). */
84
+ export function brainChanges(args) {
85
+ return brainRead("brainChanges", args, viaDoorGrant);
86
+ }
87
+
88
+ /** → { ok, lists, error }, as lib/brain.js brainLists(). */
89
+ export function brainLists(args) {
90
+ return brainRead("brainLists", args, viaDoorGrant);
91
+ }
92
+
93
+ /** → { ok, list, records, next, error }, as lib/brain.js listMembers(). */
94
+ export function listMembers(args) {
95
+ return brainRead("listMembers", args, viaDoorGrant);
96
+ }