@sprigr/apps-app-sdk 0.8.3 → 0.8.5

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
@@ -68,11 +68,12 @@ Three platform tiers, and they are not substitutes for each other. Full guide: [
68
68
  | T3 undo | handler returns `_undo`; manifest `undo.reverse_tool` | platform token | `requireApproval` (capture), `undoEnvelope`, `runUndoApply`, `@sprigr/apps-undo-journal` |
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
+ - `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.
71
72
  - `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.
72
73
  - `undoEnvelope({...})` throws on a blank field. The platform drops the whole envelope on a blank and mints nothing, silently.
73
74
  - `refuseWithoutForce(name, handler, opts)` / `archiveOfferRefusal(...)`: a delete refuses once and offers the vendor's native archive unless `force: true`.
74
75
  - `buildConfirmationPolicy({ irreversible, always, rules })` declares a dispatcher's policy beside its action registry; `checkConfirmationPolicy({ policy, registry, ungated, requiredInput })` returns every silent failure (dead rule, `(unset)` placeholder, money threshold, unclassified write) for a one-line test.
75
- - `runUndoApply(args, { env, journal, specs, pin })` is the body of your `internal: true` reverse tool: load, re-pin to the journalled connection, restore, drop. `pin` may hand `restore` a different type from the env (a per-actor token state), may throw (`connection_unavailable`) or return null to refuse (`connection_mismatch`); a restore that throws is `restore_failed` and the copy is kept.
76
+ - `runUndoApply(args, { env, journal, specs, pin })` is the body of your `internal: true` reverse tool: load, re-pin to the journalled connection, restore, drop. `pin` may hand `restore` a different type from the env (a per-actor token state), may throw (`connection_unavailable`) or return null to refuse (`connection_mismatch`); a restore that throws is `restore_failed` and the copy is kept; a successful restore may return `newId`, `notRestored`, `extra` (spread into the payload) and `note` (appended after the fidelity sentence).
76
77
  - `safeCapture(scope, name, id, read)` and `offerUndo({ journal, entity, id, before, connection, fidelity, resource, describe, warning })`: the capture-then-offer halves for a write that is not behind an approval card (a dispatcher `update_*` action). `requireApproval` uses the same two on its granted pass.
77
78
  - `sprigr-check-write-protection` (bin): every destructive-by-name tool or enumerated dispatcher action must carry a policy, or a reason in `apps/<app>/write-protection.json`; ratchets on `tools/write-protection-baseline.json`.
78
79
 
package/dist/index.d.ts CHANGED
@@ -1047,7 +1047,12 @@ declare function refuseWithoutForce<E>(deleteAction: string, inner: ToolHandler<
1047
1047
 
1048
1048
  /**
1049
1049
  * Human approval for the writes a vendor cannot undo, plus the before-image
1050
- * capture for the ones it can partly undo, in ONE wrapper.
1050
+ * capture for the ones it can partly undo, in ONE wrapper. Two shapes over the
1051
+ * same core:
1052
+ *
1053
+ * requireApproval(handlers, specs, opts) flat tools, one handler per tool
1054
+ * dispatcherApproval(specs, opts) one tool, many actions; the
1055
+ * dispatcher calls gate.run(...)
1051
1056
  *
1052
1057
  * ## Why this exists when the manifest already declares `confirmation`
1053
1058
  *
@@ -1078,8 +1083,8 @@ declare function refuseWithoutForce<E>(deleteAction: string, inner: ToolHandler<
1078
1083
  * questions stay in the app, because only the app knows what cannot be walked
1079
1084
  * back.
1080
1085
  */
1081
- /** Per-tool spec: how the card reads, and (optionally) how to journal a before-image. */
1082
- interface ApprovalSpec<E = unknown> {
1086
+ /** Per-tool (or per-action) spec: how the card reads, and (optionally) how to journal a before-image. */
1087
+ interface ApprovalSpec<P = unknown> {
1083
1088
  /**
1084
1089
  * Arg names that may carry the target id, most specific first. ONE list, used
1085
1090
  * for both the card label and the grant hash, so the two cannot drift apart
@@ -1091,7 +1096,7 @@ interface ApprovalSpec<E = unknown> {
1091
1096
  * `connection` the resolved store / organisation / business, or '' when the
1092
1097
  * app has one. Do not set `hash` here; the wrapper derives it from the raw
1093
1098
  * id and the connection so it cannot move between the ask and the retry.
1094
- * Override with `hash` below only when the arguments understate the blast
1099
+ * Extend with `hash` below only when the arguments understate the blast
1095
1100
  * radius.
1096
1101
  */
1097
1102
  describe: (target: string, args: ToolArgs, connection: string) => Omit<AppApprovalEnvelope, 'hash'>;
@@ -1101,14 +1106,14 @@ interface ApprovalSpec<E = unknown> {
1101
1106
  */
1102
1107
  hash?: (args: ToolArgs) => Array<string | number | undefined | null>;
1103
1108
  /** Present only for tools whose write can be (partly) reversed. */
1104
- undo?: UndoCaptureSpec<E>;
1109
+ undo?: UndoCaptureSpec<P>;
1105
1110
  }
1106
- interface UndoCaptureSpec<E = unknown> {
1111
+ interface UndoCaptureSpec<P = unknown> {
1107
1112
  /** Coarse type surfaced as `_undo.resource`. */
1108
1113
  resource: string;
1109
1114
  fidelity: UndoFidelity;
1110
- /** Read the object BEFORE the write, through the PINNED env. Null or throw on miss. */
1111
- capture: (pinnedEnv: E, id: string, args: ToolArgs) => Promise<unknown>;
1115
+ /** Read the object BEFORE the write, through the PINNED env or state. Null or throw on miss. */
1116
+ capture: (pinned: P, id: string, args: ToolArgs) => Promise<unknown>;
1112
1117
  /** How the change reads in `list_undoable_changes`; the connection is appended by the wrapper. */
1113
1118
  describe: (before: Record<string, unknown>, id: string) => string;
1114
1119
  /** Relayed verbatim; on `'recreated'` it must say the id changes. Never empty. */
@@ -1125,7 +1130,12 @@ interface CaptureJournal {
1125
1130
  ref: string;
1126
1131
  } | null>;
1127
1132
  }
1128
- interface RequireApprovalOptions<E = unknown> {
1133
+ /**
1134
+ * `E` is the env the tool receives; `P` is what the pinned connection looks
1135
+ * like to `capture` and `describeTarget` (the same env re-pinned for Shopify,
1136
+ * a fresh per-actor token state for Xero or Asana).
1137
+ */
1138
+ interface ApprovalGateOptions<E, P = E> {
1129
1139
  /** Log prefix, e.g. `shopify-undo`. */
1130
1140
  scope: string;
1131
1141
  /**
@@ -1135,14 +1145,19 @@ interface RequireApprovalOptions<E = unknown> {
1135
1145
  * the arguments alone: on the retry the model usually omits it.
1136
1146
  */
1137
1147
  resolveConnection: (env: E, args: ToolArgs) => Promise<string>;
1138
- /** Re-pin the env to a resolved connection so lookups and captures hit it. */
1139
- pinEnv: (env: E, connection: string) => E;
1140
1148
  /**
1141
- * Human label for a raw id, looked up THROUGH the pinned env. Fall back to
1142
- * the id on any miss; never guess, a confidently wrong card is worse than a
1143
- * terse one. Omit to always show the raw id.
1149
+ * Re-pin to a resolved connection so lookups and captures hit it. Omit when
1150
+ * `P` is `E` and the env needs no pin (a single-connection app).
1151
+ */
1152
+ pinEnv?: (env: E, connection: string) => P | Promise<P>;
1153
+ /**
1154
+ * Human label for a raw id, looked up THROUGH the pinned connection. Fall
1155
+ * back to the id on any miss; never guess, a confidently wrong card is worse
1156
+ * than a terse one. Omit to always show the raw id. `name` is the tool, or
1157
+ * for a dispatcher the action, so a lookup can route by resource type when
1158
+ * the id alone does not say what it is (an Asana gid).
1144
1159
  */
1145
- describeTarget?: (pinnedEnv: E, id: string) => Promise<string>;
1160
+ describeTarget?: (pinned: P, id: string, name: string) => Promise<string>;
1146
1161
  /**
1147
1162
  * Where before-images go. Required when any spec declares `undo`. The
1148
1163
  * journal's own capture returns null rather than throwing, and this wrapper
@@ -1155,8 +1170,29 @@ interface RequireApprovalOptions<E = unknown> {
1155
1170
  */
1156
1171
  stampConnection?: (result: unknown, connection: string) => unknown;
1157
1172
  }
1173
+ /** Flat-tool options: the pinned type is the env itself. */
1174
+ type RequireApprovalOptions<E> = ApprovalGateOptions<E, E>;
1175
+ interface DispatcherApprovalOptions<E, P = E> extends ApprovalGateOptions<E, P> {
1176
+ /**
1177
+ * Where the per-action params live on the dispatched args (`{ action,
1178
+ * input: {...} }` -> 'input'). When the field is absent the top-level args
1179
+ * are used, which is the flat-verb envelope. Default 'input'.
1180
+ */
1181
+ inputField?: string;
1182
+ }
1183
+ interface DispatcherApprovalGate<E> {
1184
+ /** True when `action` carries an approval spec. */
1185
+ has(action: string): boolean;
1186
+ /**
1187
+ * Run one dispatched action through the gate. With no spec, `write` just
1188
+ * runs. With a spec, the ask pass returns the `_approval` card and never
1189
+ * calls `write`; the granted pass captures, calls `write`, and offers
1190
+ * `_undo` beside its result.
1191
+ */
1192
+ run(action: string, args: ToolArgs, env: E, write: () => Promise<unknown>): Promise<unknown>;
1193
+ }
1158
1194
  /**
1159
- * Wrap the named tools so each asks a human first, then (if it declares
1195
+ * Wrap the named flat tools so each asks a human first, then (if it declares
1160
1196
  * `undo`) journals a before-image on the granted pass and offers `_undo`.
1161
1197
  *
1162
1198
  * Returns only the wrapped entries. Register them AFTER the originals
@@ -1168,6 +1204,20 @@ interface RequireApprovalOptions<E = unknown> {
1168
1204
  * guard exists to prevent, and it would look identical to working.
1169
1205
  */
1170
1206
  declare function requireApproval<E>(handlers: Record<string, ToolHandler<E>>, specs: Record<string, ApprovalSpec<E>>, opts: RequireApprovalOptions<E>): Record<string, ToolHandler<E>>;
1207
+ /**
1208
+ * The same gate for a dispatcher tool (one tool, many actions selected by an
1209
+ * input field). Build it once beside the action registry and call `run` from
1210
+ * the dispatcher after the action is resolved and its params parsed:
1211
+ *
1212
+ * const gate = dispatcherApproval(SPECS, { scope, resolveConnection, pinEnv, journal });
1213
+ * ...
1214
+ * return gate.run(action, args, env, () => def.execute(state, parsed));
1215
+ *
1216
+ * The platform stamps `_approval_granted` on the TOP-LEVEL args; the per-action
1217
+ * params are read from `opts.inputField` (default `input`), or from the
1218
+ * top-level args when that field is absent (the flat-verb envelope).
1219
+ */
1220
+ declare function dispatcherApproval<E, P = E>(specs: Record<string, ApprovalSpec<P>>, opts: DispatcherApprovalOptions<E, P>): DispatcherApprovalGate<E>;
1171
1221
 
1172
1222
  /**
1173
1223
  * The reversal entry point every adopting app declares as `undo.reverse_tool`.
@@ -1221,6 +1271,13 @@ interface RestoreResult {
1221
1271
  error?: string;
1222
1272
  notRestored?: string;
1223
1273
  extra?: Record<string, unknown>;
1274
+ /**
1275
+ * Free-form detail appended to the runner's note (the new gid, rows it
1276
+ * could not refile, a sticker the vendor will not remove). The runner's own
1277
+ * sentence on fidelity stays first so the model reads "replacement, not an
1278
+ * undo" before the detail.
1279
+ */
1280
+ note?: string;
1224
1281
  }
1225
1282
  interface JournalRowLike<T = unknown> {
1226
1283
  entity: string;
@@ -1767,4 +1824,4 @@ interface ExecutionContextLike {
1767
1824
  }
1768
1825
  type HandlerFn<TArgs, TResult> = (args: TArgs, env: Record<string, unknown>, ctx?: ExecutionContextLike) => Promise<TResult>;
1769
1826
 
1770
- export { APPROVAL_GRANTED_KEY, type Actor, type AppApprovalEnvelope, type AppFileListItem, type AppFileUrlArgs, type AppFileUrlResult, type AppFilesEnv, type AppUndoEnvelope, type ApprovalSpec, type ArchiveOfferOptions, type ArchiveOfferRefusal, type CaptureJournal, type ConfirmCondition, type ConfirmRule, type ConfirmationPolicy, type D1Like, type D1PreparedStatementLike, DEFAULT_EMIT_TIMEOUT_MS, DEFAULT_MAX_FILE_BYTES, DEFAULT_MONEY_FIELDS, DEFAULT_WRITE_PREFIXES, type EmitMarketplaceEventOptions, type EmitResult, type EmitTransport, type EventArgs, type ExecutionContextLike, type FetchFileResult, type FetchWithRetryOptions, type GetAppFileResult, type HandlerFn, type InboxOwner, type InstallBridge, InvalidUndoEnvelopeError, type JournalRowLike, type MarketplaceEventEmitOpts, type MarketplaceEventSourceIntegration, NotConnectedError, type OAuthBouncerSignals, type OAuthState, type OfferUndoArgs, type PlatformHostEnv, type PolicyCheckInput, type PolicySource, type PutAppFileArgs, type PutAppFileResult, type PutAppFileStreamArgs, type RequireApprovalOptions, type RestoreResult, type RestoreSpec, type ScheduleArgs, type SprigrDataAggregationCell, type SprigrDataAggregationResult, type SprigrDataAggregationSpec, type SprigrDataApi, type SprigrDataIndexOpts, type SprigrDataSearchOpts, type SprigrDataSearchResult, type SprigrFilesApi, type SprigrFilesApiWithEdit, type SprigrFilesCreateInput, type SprigrFilesCreateResult, type SprigrFilesEditInput, type SprigrFilesEditResult, type SprigrFilesJobResult, type ToolArgs, type ToolHandler, type ToolResult, type ToolWrapperOptions, UNIT_SEP, type UndoApplyOptions, type UndoApplyResult, type UndoCaptureSpec, type UndoFidelity, type WebhookArgs, type WfpBridgeEnv, type WfpEmitReply, actorKey, actorTool, appFileUrl, approvalHash, archiveOfferRefusal, base64ToBytes, buildConfirmationPolicy, buildMarketplaceWebhookUrl, bytesToBase64, canEmit, checkConfirmationPolicy, constantTimeEqual, createMarketplaceEmitter, createToolWrappers, decodeState, deleteAppFile, describeMissingBridge, emitMarketplaceEvent, encodeState, fetchFileAsBase64, fetchFileBytes, fetchWithRetry, fullWarning, getAppFile, hmacSha256Hex, installTokenPost, isNotConnectedError, isStagingHost, listAppFiles, offerUndo, overlaySprigr, ownerFromActorKey, parseActor, putAppFile, putAppFileStream, randomHex, recreatedWarning, refuseWithoutForce, requireApproval, resolveInstallBridge, resolveOAuthBouncerBase, resolveOAuthBouncerCallbackUrl, resolvePlatformWebhookBase, runUndoApply, safeCapture, seq, set, tool, undoEnvelope, withSprigrEmitFallback };
1827
+ export { APPROVAL_GRANTED_KEY, type Actor, type AppApprovalEnvelope, type AppFileListItem, type AppFileUrlArgs, type AppFileUrlResult, type AppFilesEnv, type AppUndoEnvelope, type ApprovalGateOptions, type ApprovalSpec, type ArchiveOfferOptions, type ArchiveOfferRefusal, type CaptureJournal, type ConfirmCondition, type ConfirmRule, type ConfirmationPolicy, type D1Like, type D1PreparedStatementLike, DEFAULT_EMIT_TIMEOUT_MS, DEFAULT_MAX_FILE_BYTES, DEFAULT_MONEY_FIELDS, DEFAULT_WRITE_PREFIXES, type DispatcherApprovalGate, type DispatcherApprovalOptions, type EmitMarketplaceEventOptions, type EmitResult, type EmitTransport, type EventArgs, type ExecutionContextLike, type FetchFileResult, type FetchWithRetryOptions, type GetAppFileResult, type HandlerFn, type InboxOwner, type InstallBridge, InvalidUndoEnvelopeError, type JournalRowLike, type MarketplaceEventEmitOpts, type MarketplaceEventSourceIntegration, NotConnectedError, type OAuthBouncerSignals, type OAuthState, type OfferUndoArgs, type PlatformHostEnv, type PolicyCheckInput, type PolicySource, type PutAppFileArgs, type PutAppFileResult, type PutAppFileStreamArgs, type RequireApprovalOptions, type RestoreResult, type RestoreSpec, type ScheduleArgs, type SprigrDataAggregationCell, type SprigrDataAggregationResult, type SprigrDataAggregationSpec, type SprigrDataApi, type SprigrDataIndexOpts, type SprigrDataSearchOpts, type SprigrDataSearchResult, type SprigrFilesApi, type SprigrFilesApiWithEdit, type SprigrFilesCreateInput, type SprigrFilesCreateResult, type SprigrFilesEditInput, type SprigrFilesEditResult, type SprigrFilesJobResult, type ToolArgs, type ToolHandler, type ToolResult, type ToolWrapperOptions, UNIT_SEP, type UndoApplyOptions, type UndoApplyResult, type UndoCaptureSpec, type UndoFidelity, type WebhookArgs, type WfpBridgeEnv, type WfpEmitReply, actorKey, actorTool, appFileUrl, approvalHash, archiveOfferRefusal, base64ToBytes, buildConfirmationPolicy, buildMarketplaceWebhookUrl, bytesToBase64, canEmit, checkConfirmationPolicy, constantTimeEqual, createMarketplaceEmitter, createToolWrappers, decodeState, deleteAppFile, describeMissingBridge, dispatcherApproval, emitMarketplaceEvent, encodeState, fetchFileAsBase64, fetchFileBytes, fetchWithRetry, fullWarning, getAppFile, hmacSha256Hex, installTokenPost, isNotConnectedError, isStagingHost, listAppFiles, offerUndo, overlaySprigr, ownerFromActorKey, parseActor, putAppFile, putAppFileStream, randomHex, recreatedWarning, refuseWithoutForce, requireApproval, resolveInstallBridge, resolveOAuthBouncerBase, resolveOAuthBouncerCallbackUrl, resolvePlatformWebhookBase, runUndoApply, safeCapture, seq, set, tool, undoEnvelope, withSprigrEmitFallback };
package/dist/index.js CHANGED
@@ -617,57 +617,41 @@ function rawIdOf(args, keys) {
617
617
  function isOk(result) {
618
618
  return !!result && typeof result === "object" && result.ok !== false;
619
619
  }
620
- function requireApproval(handlers, specs, opts) {
621
- const gated = {};
622
- for (const [name, spec] of Object.entries(specs)) {
623
- const inner = handlers[name];
624
- if (!inner) {
625
- throw new Error(
626
- `requireApproval: "${name}" has an approval spec but is not in the handler registry. It was probably renamed or removed; update the specs in the same change, or a destructive tool ships ungated.`
627
- );
628
- }
629
- if (spec.undo && !opts.journal) {
630
- throw new Error(`requireApproval: "${name}" declares undo but no journal was supplied.`);
631
- }
632
- gated[name] = async (args, env, ctx) => {
633
- const a = args ?? {};
634
- if (a[APPROVAL_GRANTED_KEY] === true) {
635
- const { [APPROVAL_GRANTED_KEY]: _granted, ...rest } = a;
636
- return granted(name, spec, rest, env, ctx, inner, opts);
637
- }
638
- const connection = await opts.resolveConnection(env, a);
639
- const rawId = rawIdOf(a, spec.keys);
640
- const pinned = connection ? opts.pinEnv(env, connection) : env;
641
- const target = rawId && opts.describeTarget ? await safeLabel(opts.describeTarget, pinned, rawId) : rawId || "(unknown id)";
642
- const extra = spec.hash ? spec.hash(a) : [];
643
- return {
644
- ok: false,
645
- _approval: {
646
- ...spec.describe(target, a, connection),
647
- hash: approvalHash(rawId, connection, ...extra)
648
- }
649
- };
650
- };
651
- }
652
- return gated;
653
- }
654
- async function safeLabel(fn, env, id) {
620
+ async function safeLabel(fn, pinned, id, name) {
655
621
  try {
656
- const label = await fn(env, id);
622
+ const label = await fn(pinned, id, name);
657
623
  return label && label.trim() ? label : id;
658
624
  } catch {
659
625
  return id;
660
626
  }
661
627
  }
662
- async function granted(name, spec, args, env, ctx, inner, opts) {
663
- const connection = await opts.resolveConnection(env, args);
628
+ async function pin(opts, env, connection) {
629
+ if (!connection || !opts.pinEnv) return env;
630
+ return opts.pinEnv(env, connection);
631
+ }
632
+ async function askPass(name, spec, params, env, opts, hashPrefix) {
633
+ const connection = await opts.resolveConnection(env, params);
634
+ const rawId = rawIdOf(params, spec.keys);
635
+ const pinned = await pin(opts, env, connection);
636
+ const target = rawId && opts.describeTarget ? await safeLabel(opts.describeTarget, pinned, rawId, name) : rawId || "(unknown id)";
637
+ const extra = spec.hash ? spec.hash(params) : [];
638
+ return {
639
+ ok: false,
640
+ _approval: {
641
+ ...spec.describe(target, params, connection),
642
+ hash: approvalHash(...hashPrefix, rawId, connection, ...extra)
643
+ }
644
+ };
645
+ }
646
+ async function grantedPass(name, spec, params, env, opts, write) {
647
+ const connection = await opts.resolveConnection(env, params);
664
648
  const stamp = (r) => connection && opts.stampConnection ? opts.stampConnection(r, connection) : r;
665
649
  const undo = spec.undo;
666
- const id = rawIdOf(args, spec.keys);
667
- if (!undo || !id) return stamp(await inner(args, env, ctx));
668
- const pinned = connection ? opts.pinEnv(env, connection) : env;
669
- const before = await safeCapture(opts.scope, name, id, () => undo.capture(pinned, id, args));
670
- const result = await inner(args, env, ctx);
650
+ const id = rawIdOf(params, spec.keys);
651
+ if (!undo || !id) return stamp(await write());
652
+ const pinned = await pin(opts, env, connection);
653
+ const before = await safeCapture(opts.scope, name, id, () => undo.capture(pinned, id, params));
654
+ const result = await write();
671
655
  if (!isOk(result) || !before) return stamp(result);
672
656
  const envelope = await offerUndo({
673
657
  journal: opts.journal(env),
@@ -683,6 +667,50 @@ async function granted(name, spec, args, env, ctx, inner, opts) {
683
667
  if (!envelope) return stamp(result);
684
668
  return { ...stamp(result), _undo: envelope };
685
669
  }
670
+ function assertJournal(name, spec, opts, who) {
671
+ if (spec.undo && !opts.journal) {
672
+ throw new Error(`${who}: "${name}" declares undo but no journal was supplied.`);
673
+ }
674
+ }
675
+ function requireApproval(handlers, specs, opts) {
676
+ const gated = {};
677
+ for (const [name, spec] of Object.entries(specs)) {
678
+ const inner = handlers[name];
679
+ if (!inner) {
680
+ throw new Error(
681
+ `requireApproval: "${name}" has an approval spec but is not in the handler registry. It was probably renamed or removed; update the specs in the same change, or a destructive tool ships ungated.`
682
+ );
683
+ }
684
+ assertJournal(name, spec, opts, "requireApproval");
685
+ gated[name] = async (args, env, ctx) => {
686
+ const a = args ?? {};
687
+ if (a[APPROVAL_GRANTED_KEY] === true) {
688
+ const { [APPROVAL_GRANTED_KEY]: _granted, ...rest } = a;
689
+ return grantedPass(name, spec, rest, env, opts, () => inner(rest, env, ctx));
690
+ }
691
+ return askPass(name, spec, a, env, opts, []);
692
+ };
693
+ }
694
+ return gated;
695
+ }
696
+ function dispatcherApproval(specs, opts) {
697
+ for (const [name, spec] of Object.entries(specs)) assertJournal(name, spec, opts, "dispatcherApproval");
698
+ const field = opts.inputField ?? "input";
699
+ return {
700
+ has: (action) => Object.prototype.hasOwnProperty.call(specs, action),
701
+ async run(action, args, env, write) {
702
+ const spec = specs[action];
703
+ if (!spec) return write();
704
+ const a = args ?? {};
705
+ const nested = a[field];
706
+ const params = nested && typeof nested === "object" && !Array.isArray(nested) ? nested : a;
707
+ if (a[APPROVAL_GRANTED_KEY] === true) {
708
+ return grantedPass(action, spec, params, env, opts, write);
709
+ }
710
+ return askPass(action, spec, params, env, opts, [action]);
711
+ }
712
+ };
713
+ }
686
714
 
687
715
  // src/write-protection/undo-apply.ts
688
716
  async function runUndoApply(args, opts) {
@@ -705,7 +733,7 @@ async function runUndoApply(args, opts) {
705
733
  };
706
734
  }
707
735
  const before = row.before;
708
- if (!before || typeof before !== "object") {
736
+ if (!before || typeof before !== "object" || Array.isArray(before)) {
709
737
  return { ok: false, error: "before_image_unreadable", note: "The saved copy could not be read back. Nothing was changed." };
710
738
  }
711
739
  let pinned;
@@ -741,6 +769,7 @@ async function runUndoApply(args, opts) {
741
769
  await opts.journal.dropBefore(ref);
742
770
  const recreated = spec.fidelity === "recreated";
743
771
  const notRestored = result.notRestored ?? spec.notRestored;
772
+ const suffix = result.note && result.note.trim() ? ` ${result.note.trim()}` : "";
744
773
  return {
745
774
  ok: true,
746
775
  ...result.extra ?? {},
@@ -750,7 +779,7 @@ async function runUndoApply(args, opts) {
750
779
  new_id: result.newId ?? null,
751
780
  fidelity: spec.fidelity,
752
781
  ...notRestored ? { not_restored: notRestored } : {},
753
- note: recreated ? `Created a NEW ${spec.resource}${result.newId ? ` (${result.newId})` : ""} from the saved copy. The original ${row.original_id} is still gone and its id will never be reissued.${notRestored ? ` Not restored: ${notRestored}.` : ""} Report this as a replacement, not as an undo.` : `Restored ${spec.resource} ${row.original_id} to its saved values.${notRestored ? ` Not restored: ${notRestored}.` : ""} Anything changed since the capture was overwritten.`
782
+ note: recreated ? `Created a NEW ${spec.resource}${result.newId ? ` (${result.newId})` : ""} from the saved copy. The original ${row.original_id} is still gone and its id will never be reissued.${notRestored ? ` Not restored: ${notRestored}.` : ""} Report this as a replacement, not as an undo.${suffix}` : `Restored ${spec.resource} ${row.original_id} to its saved values.${notRestored ? ` Not restored: ${notRestored}.` : ""} Anything changed since the capture was overwritten.${suffix}`
754
783
  };
755
784
  }
756
785
 
@@ -937,6 +966,7 @@ export {
937
966
  decodeState,
938
967
  deleteAppFile,
939
968
  describeMissingBridge,
969
+ dispatcherApproval,
940
970
  emitMarketplaceEvent,
941
971
  encodeState,
942
972
  fetchFileAsBase64,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprigr/apps-app-sdk",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",