@sprigr/apps-app-sdk 0.22.0 → 0.23.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
@@ -95,6 +95,7 @@ Three platform tiers, and they are not substitutes for each other. Full guide: [
95
95
 
96
96
  - `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))`.
97
97
  - `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.
98
+ - A spec with no `hash` binds the grant to its payload: every argument except the `keys` fields, the attestation flags and the gate's `connectionArgs` (e.g. `['store']`) goes into the hash canonically, so a tap on one payload cannot be spent on another (sprigr-apps#2605). See docs/write-protection.md.
98
99
  - 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 `''`.
99
100
  - `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.
100
101
  - `undoEnvelope({...})` throws on a blank field. The platform drops the whole envelope on a blank and mints nothing, silently.
package/dist/index.d.ts CHANGED
@@ -1890,6 +1890,14 @@ interface ApprovalSpec<P = unknown, E = unknown> {
1890
1890
  /**
1891
1891
  * Extra identity parts beyond `rawId + connection`, e.g. a sorted tag set.
1892
1892
  * Use `set()` / `seq()` from `approval-hash`. Never include `confirm`.
1893
+ *
1894
+ * Omitted, the wrapper appends a canonical form of every OTHER argument:
1895
+ * not the `keys` fields, not `confirm` / `_approval_granted`, and not the
1896
+ * gate's `connectionArgs` (sprigr-apps#2605). So a grant approved for one
1897
+ * payload cannot be spent on another; before, a hash-less refund hashed the
1898
+ * same for 10.00 and 250.00. Supply `hash` when that default is too strict
1899
+ * (an order-insensitive set, a free-text field the model rewords) or
1900
+ * `() => []` when the id and connection really are the whole operation.
1893
1901
  */
1894
1902
  hash?: (args: ToolArgs) => Array<string | number | undefined | null>;
1895
1903
  /**
@@ -1972,6 +1980,14 @@ interface ApprovalGateOptions<E, P = E> {
1972
1980
  * `ApprovalSpec.resolveConnection`.
1973
1981
  */
1974
1982
  resolveConnection: (env: E, args: ToolArgs) => Promise<string>;
1983
+ /**
1984
+ * Argument names that only CHOOSE the connection (e.g. `['store']`). They
1985
+ * are left out of the default payload hash (see `ApprovalSpec.hash`)
1986
+ * because the connection is already in the hash in its RESOLVED form, and
1987
+ * the model usually omits the raw argument on the retry. Omit for a
1988
+ * single-connection app.
1989
+ */
1990
+ connectionArgs?: string[];
1975
1991
  /**
1976
1992
  * Re-pin to a resolved connection so lookups and captures hit it. Omit when
1977
1993
  * `P` is `E` and the env needs no pin (a single-connection app).
package/dist/index.js CHANGED
@@ -1470,6 +1470,23 @@ async function offerUndo(args) {
1470
1470
  }
1471
1471
 
1472
1472
  // src/write-protection/require-approval.ts
1473
+ var ATTESTATION_KEYS = ["confirm", APPROVAL_GRANTED_KEY];
1474
+ function canonicalJson(value) {
1475
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
1476
+ if (value && typeof value === "object") {
1477
+ const entries = Object.keys(value).filter((k) => value[k] !== void 0).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(value[k])}`);
1478
+ return `{${entries.join(",")}}`;
1479
+ }
1480
+ return JSON.stringify(value ?? null);
1481
+ }
1482
+ function defaultPayloadParts(params, keys, connectionArgs = []) {
1483
+ const skip = /* @__PURE__ */ new Set([...keys, ...ATTESTATION_KEYS, ...connectionArgs]);
1484
+ const rest = {};
1485
+ for (const [k, v] of Object.entries(params)) {
1486
+ if (!skip.has(k) && v !== void 0) rest[k] = v;
1487
+ }
1488
+ return Object.keys(rest).length ? [canonicalJson(rest)] : [];
1489
+ }
1473
1490
  function rawIdOf(args, keys) {
1474
1491
  for (const k of keys) {
1475
1492
  const v = args[k];
@@ -1509,7 +1526,7 @@ async function askPass(name, spec, params, env, opts, hashPrefix) {
1509
1526
  const pinned = await pin(opts, env, connection);
1510
1527
  const describeTarget = spec.describeTarget ?? opts.describeTarget;
1511
1528
  const target = rawId && describeTarget ? await safeLabel(describeTarget, pinned, rawId, name) : rawId || "(unknown id)";
1512
- const extra = spec.hash ? spec.hash(params) : [];
1529
+ const extra = spec.hash ? spec.hash(params) : defaultPayloadParts(params, spec.keys, opts.connectionArgs);
1513
1530
  const count = countOf(spec, params, rawId);
1514
1531
  return {
1515
1532
  ok: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprigr/apps-app-sdk",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",