@sprigr/apps-app-sdk 0.9.0 → 0.10.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.
package/README.md CHANGED
@@ -69,6 +69,7 @@ Three platform tiers, and they are not substitutes for each other. Full guide: [
69
69
 
70
70
  - `requireApproval(handlers, specs, opts)` wraps the named tools so each returns an `_approval` card instead of writing, then on the granted pass captures a before-image through the **pinned** connection and offers `_undo`. It resolves the connection inside itself, before the label lookup, the hash and the capture: a gate that sits outside the app's own pin names the wrong store, hashes the wrong store and captures the wrong store's copy. Register the result after the originals: `Object.assign(registry, requireApproval(registry, SPECS, opts))`.
71
71
  - `dispatcherApproval(specs, opts)`: the same gate for a one-tool-many-actions dispatcher. Build it beside the action registry and call `gate.run(action, args, env, () => def.execute(state, parsed))` from the dispatcher; params are read from `opts.inputField` (default `input`), the action name is hashed in, and `pinEnv` may return a per-actor token state rather than the env.
72
+ - Every resolver on the gate is a **default, overridable per spec**: `ApprovalSpec.resolveConnection`, `.describeTarget` and `.stampConnection` win over the gate's for that spec alone. Reach for it when one app has two families of write that reach a connection differently (creates that POST under an account, deletes that address a global object id on a path carrying no account). One gate-level rule has to pick one of them, and for the other it prints a false connection on the card a person taps and mixes it into the grant hash. A spec that hits no connection returns `''`.
72
73
  - `approvalHash(rawId, connection, ...parts)`, `set(values)`, `seq(values)`: a stable operation identity so a tap survives the model reordering a list or dropping an optional argument on the retry.
73
74
  - `undoEnvelope({...})` throws on a blank field. The platform drops the whole envelope on a blank and mints nothing, silently.
74
75
  - `refuseWithoutForce(name, handler, opts)` / `archiveOfferRefusal(...)`: a delete refuses once and offers the vendor's native archive unless `force: true`.
package/dist/index.d.ts CHANGED
@@ -1250,12 +1250,25 @@ declare function refuseWithoutForce<E>(deleteAction: string, inner: ToolHandler<
1250
1250
  * `_undo` ON A NULL CAPTURE. A failed capture does not fail the write: the
1251
1251
  * user asked for it and a human approved it. It degrades to no token.
1252
1252
  *
1253
+ * Every resolver here is gate-level by default and overridable per spec:
1254
+ * `ApprovalSpec.resolveConnection` / `.describeTarget` / `.stampConnection`
1255
+ * win over the gate's for that spec alone. One rule cannot describe two
1256
+ * families of write truthfully, and both consumers of the resolved value are
1257
+ * safety surfaces (the card a person reads, and the grant hash).
1258
+ *
1253
1259
  * Generalised from the Shopify app's `require-approval.ts`; the per-tool
1254
1260
  * questions stay in the app, because only the app knows what cannot be walked
1255
1261
  * back.
1256
1262
  */
1257
- /** Per-tool (or per-action) spec: how the card reads, and (optionally) how to journal a before-image. */
1258
- interface ApprovalSpec<P = unknown> {
1263
+ /**
1264
+ * Per-tool (or per-action) spec: how the card reads, and (optionally) how to
1265
+ * journal a before-image.
1266
+ *
1267
+ * `P` is the pinned connection handed to `capture` / `describeTarget`; `E` is
1268
+ * the env a spec-level `resolveConnection` receives. `E` defaults to `unknown`
1269
+ * so `ApprovalSpec<MyState>` keeps working unchanged.
1270
+ */
1271
+ interface ApprovalSpec<P = unknown, E = unknown> {
1259
1272
  /**
1260
1273
  * Arg names that may carry the target id, most specific first. ONE list, used
1261
1274
  * for both the card label and the grant hash, so the two cannot drift apart
@@ -1284,6 +1297,27 @@ interface ApprovalSpec<P = unknown> {
1284
1297
  * can ever cover it, which is the safe side but not the useful one.
1285
1298
  */
1286
1299
  count?: (args: ToolArgs) => number;
1300
+ /**
1301
+ * Overrides the gate's `resolveConnection` for THIS spec only; the gate's
1302
+ * value stays the default for every spec that omits it.
1303
+ *
1304
+ * One gate serves every spec, so a gate-level resolver has to pick one rule.
1305
+ * That is wrong the moment an app has two families of write that reach a
1306
+ * connection differently: in meta-ads (sprigr-apps#1462) six creates POST
1307
+ * under an ad account and three address a Graph object by id on a path that
1308
+ * carries no account at all, so the account resolved for the creates was
1309
+ * printed on the delete's card and mixed into its hash while being false.
1310
+ * A resolver returning '' here is the honest answer for such a spec.
1311
+ */
1312
+ resolveConnection?: (env: E, args: ToolArgs) => Promise<string>;
1313
+ /** Overrides the gate's `describeTarget` for this spec only. */
1314
+ describeTarget?: (pinned: P, id: string, name: string) => Promise<string>;
1315
+ /**
1316
+ * Overrides the gate's `stampConnection` for this spec only. The stamp
1317
+ * repeats the card's claim in the agent's "done" report, so a spec that
1318
+ * resolves its own connection usually wants to stamp it its own way too.
1319
+ */
1320
+ stampConnection?: (result: unknown, connection: string) => unknown;
1287
1321
  /** Present only for tools whose write can be (partly) reversed. */
1288
1322
  undo?: UndoCaptureSpec<P>;
1289
1323
  }
@@ -1322,6 +1356,10 @@ interface ApprovalGateOptions<E, P = E> {
1322
1356
  * arguments and the install, canonicalised (a label like "EU store" becomes
1323
1357
  * the real domain). Return '' for a single-connection app. Never read it off
1324
1358
  * the arguments alone: on the retry the model usually omits it.
1359
+ *
1360
+ * This is the DEFAULT for every spec. A spec whose write reaches a
1361
+ * connection by another rule overrides it with
1362
+ * `ApprovalSpec.resolveConnection`.
1325
1363
  */
1326
1364
  resolveConnection: (env: E, args: ToolArgs) => Promise<string>;
1327
1365
  /**
@@ -1334,7 +1372,8 @@ interface ApprovalGateOptions<E, P = E> {
1334
1372
  * back to the id on any miss; never guess, a confidently wrong card is worse
1335
1373
  * than a terse one. Omit to always show the raw id. `name` is the tool, or
1336
1374
  * for a dispatcher the action, so a lookup can route by resource type when
1337
- * the id alone does not say what it is (an Asana gid).
1375
+ * the id alone does not say what it is (an Asana gid). Overridable per
1376
+ * spec.
1338
1377
  */
1339
1378
  describeTarget?: (pinned: P, id: string, name: string) => Promise<string>;
1340
1379
  /**
@@ -1345,7 +1384,8 @@ interface ApprovalGateOptions<E, P = E> {
1345
1384
  journal?: (env: E) => CaptureJournal;
1346
1385
  /**
1347
1386
  * Stamp the resolved connection onto a successful result (e.g. `{ store }`)
1348
- * so an agent reporting "deleted it" can name where. Optional.
1387
+ * so an agent reporting "deleted it" can name where. Optional, and
1388
+ * overridable per spec.
1349
1389
  */
1350
1390
  stampConnection?: (result: unknown, connection: string) => unknown;
1351
1391
  }
@@ -1382,7 +1422,7 @@ interface DispatcherApprovalGate<E> {
1382
1422
  * rename that quietly ungated a permanent delete is exactly the failure this
1383
1423
  * guard exists to prevent, and it would look identical to working.
1384
1424
  */
1385
- declare function requireApproval<E>(handlers: Record<string, ToolHandler<E>>, specs: Record<string, ApprovalSpec<E>>, opts: RequireApprovalOptions<E>): Record<string, ToolHandler<E>>;
1425
+ declare function requireApproval<E>(handlers: Record<string, ToolHandler<E>>, specs: Record<string, ApprovalSpec<E, E>>, opts: RequireApprovalOptions<E>): Record<string, ToolHandler<E>>;
1386
1426
  /**
1387
1427
  * The same gate for a dispatcher tool (one tool, many actions selected by an
1388
1428
  * input field). Build it once beside the action registry and call `run` from
@@ -1396,7 +1436,7 @@ declare function requireApproval<E>(handlers: Record<string, ToolHandler<E>>, sp
1396
1436
  * params are read from `opts.inputField` (default `input`), or from the
1397
1437
  * top-level args when that field is absent (the flat-verb envelope).
1398
1438
  */
1399
- declare function dispatcherApproval<E, P = E>(specs: Record<string, ApprovalSpec<P>>, opts: DispatcherApprovalOptions<E, P>): DispatcherApprovalGate<E>;
1439
+ declare function dispatcherApproval<E, P = E>(specs: Record<string, ApprovalSpec<P, E>>, opts: DispatcherApprovalOptions<E, P>): DispatcherApprovalGate<E>;
1400
1440
 
1401
1441
  /**
1402
1442
  * The reversal entry point every adopting app declares as `undo.reverse_tool`.
package/dist/index.js CHANGED
@@ -758,10 +758,11 @@ async function pin(opts, env, connection) {
758
758
  return opts.pinEnv(env, connection);
759
759
  }
760
760
  async function askPass(name, spec, params, env, opts, hashPrefix) {
761
- const connection = await opts.resolveConnection(env, params);
761
+ const connection = await (spec.resolveConnection ?? opts.resolveConnection)(env, params);
762
762
  const rawId = rawIdOf(params, spec.keys);
763
763
  const pinned = await pin(opts, env, connection);
764
- const target = rawId && opts.describeTarget ? await safeLabel(opts.describeTarget, pinned, rawId, name) : rawId || "(unknown id)";
764
+ const describeTarget = spec.describeTarget ?? opts.describeTarget;
765
+ const target = rawId && describeTarget ? await safeLabel(describeTarget, pinned, rawId, name) : rawId || "(unknown id)";
765
766
  const extra = spec.hash ? spec.hash(params) : [];
766
767
  const count = countOf(spec, params, rawId);
767
768
  return {
@@ -774,8 +775,9 @@ async function askPass(name, spec, params, env, opts, hashPrefix) {
774
775
  };
775
776
  }
776
777
  async function grantedPass(name, spec, params, env, opts, write) {
777
- const connection = await opts.resolveConnection(env, params);
778
- const stamp = (r) => connection && opts.stampConnection ? opts.stampConnection(r, connection) : r;
778
+ const connection = await (spec.resolveConnection ?? opts.resolveConnection)(env, params);
779
+ const stampConnection = spec.stampConnection ?? opts.stampConnection;
780
+ const stamp = (r) => connection && stampConnection ? stampConnection(r, connection) : r;
779
781
  const undo = spec.undo;
780
782
  const id = rawIdOf(params, spec.keys);
781
783
  if (!undo || !id) return stamp(await write());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprigr/apps-app-sdk",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",