@sprigr/apps-app-sdk 0.8.2 → 0.8.4

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,6 +68,7 @@ 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`.
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,17 @@ 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.
1144
1157
  */
1145
- describeTarget?: (pinnedEnv: E, id: string) => Promise<string>;
1158
+ describeTarget?: (pinned: P, id: string) => Promise<string>;
1146
1159
  /**
1147
1160
  * Where before-images go. Required when any spec declares `undo`. The
1148
1161
  * journal's own capture returns null rather than throwing, and this wrapper
@@ -1155,8 +1168,29 @@ interface RequireApprovalOptions<E = unknown> {
1155
1168
  */
1156
1169
  stampConnection?: (result: unknown, connection: string) => unknown;
1157
1170
  }
1171
+ /** Flat-tool options: the pinned type is the env itself. */
1172
+ type RequireApprovalOptions<E> = ApprovalGateOptions<E, E>;
1173
+ interface DispatcherApprovalOptions<E, P = E> extends ApprovalGateOptions<E, P> {
1174
+ /**
1175
+ * Where the per-action params live on the dispatched args (`{ action,
1176
+ * input: {...} }` -> 'input'). When the field is absent the top-level args
1177
+ * are used, which is the flat-verb envelope. Default 'input'.
1178
+ */
1179
+ inputField?: string;
1180
+ }
1181
+ interface DispatcherApprovalGate<E> {
1182
+ /** True when `action` carries an approval spec. */
1183
+ has(action: string): boolean;
1184
+ /**
1185
+ * Run one dispatched action through the gate. With no spec, `write` just
1186
+ * runs. With a spec, the ask pass returns the `_approval` card and never
1187
+ * calls `write`; the granted pass captures, calls `write`, and offers
1188
+ * `_undo` beside its result.
1189
+ */
1190
+ run(action: string, args: ToolArgs, env: E, write: () => Promise<unknown>): Promise<unknown>;
1191
+ }
1158
1192
  /**
1159
- * Wrap the named tools so each asks a human first, then (if it declares
1193
+ * Wrap the named flat tools so each asks a human first, then (if it declares
1160
1194
  * `undo`) journals a before-image on the granted pass and offers `_undo`.
1161
1195
  *
1162
1196
  * Returns only the wrapped entries. Register them AFTER the originals
@@ -1168,6 +1202,20 @@ interface RequireApprovalOptions<E = unknown> {
1168
1202
  * guard exists to prevent, and it would look identical to working.
1169
1203
  */
1170
1204
  declare function requireApproval<E>(handlers: Record<string, ToolHandler<E>>, specs: Record<string, ApprovalSpec<E>>, opts: RequireApprovalOptions<E>): Record<string, ToolHandler<E>>;
1205
+ /**
1206
+ * The same gate for a dispatcher tool (one tool, many actions selected by an
1207
+ * input field). Build it once beside the action registry and call `run` from
1208
+ * the dispatcher after the action is resolved and its params parsed:
1209
+ *
1210
+ * const gate = dispatcherApproval(SPECS, { scope, resolveConnection, pinEnv, journal });
1211
+ * ...
1212
+ * return gate.run(action, args, env, () => def.execute(state, parsed));
1213
+ *
1214
+ * The platform stamps `_approval_granted` on the TOP-LEVEL args; the per-action
1215
+ * params are read from `opts.inputField` (default `input`), or from the
1216
+ * top-level args when that field is absent (the flat-verb envelope).
1217
+ */
1218
+ declare function dispatcherApproval<E, P = E>(specs: Record<string, ApprovalSpec<P>>, opts: DispatcherApprovalOptions<E, P>): DispatcherApprovalGate<E>;
1171
1219
 
1172
1220
  /**
1173
1221
  * The reversal entry point every adopting app declares as `undo.reverse_tool`.
@@ -1205,16 +1253,23 @@ interface RestoreSpec<P, T = Record<string, unknown>> {
1205
1253
  /**
1206
1254
  * Rebuild or write back, through the PINNED connection. May return
1207
1255
  * `{ ok: false, error }` or throw; both become `restore_failed` and the
1208
- * before-image is kept.
1256
+ * before-image is kept. A successful result may carry `notRestored`
1257
+ * (overrides the spec's static one, for a replay that only learns what it
1258
+ * could not write back while writing) and `extra`, spread into the success
1259
+ * payload for app-specific disclosure the model should read (a list of
1260
+ * dropped fields, child rows handed back for deliberate recreation).
1209
1261
  */
1210
- restore: (pinned: P, before: T, row: JournalRowLike<T>) => Promise<{
1211
- ok: boolean;
1212
- newId?: string;
1213
- error?: string;
1214
- } | void>;
1262
+ restore: (pinned: P, before: T, row: JournalRowLike<T>) => Promise<RestoreResult | void>;
1215
1263
  /** What the restore does NOT bring back, folded into the note verbatim. */
1216
1264
  notRestored?: string;
1217
1265
  }
1266
+ interface RestoreResult {
1267
+ ok: boolean;
1268
+ newId?: string;
1269
+ error?: string;
1270
+ notRestored?: string;
1271
+ extra?: Record<string, unknown>;
1272
+ }
1218
1273
  interface JournalRowLike<T = unknown> {
1219
1274
  entity: string;
1220
1275
  original_id: string;
@@ -1245,6 +1300,7 @@ type UndoApplyResult = {
1245
1300
  fidelity: UndoFidelity;
1246
1301
  not_restored?: string;
1247
1302
  note: string;
1303
+ [extra: string]: unknown;
1248
1304
  } | {
1249
1305
  ok: false;
1250
1306
  error: string;
@@ -1759,4 +1815,4 @@ interface ExecutionContextLike {
1759
1815
  }
1760
1816
  type HandlerFn<TArgs, TResult> = (args: TArgs, env: Record<string, unknown>, ctx?: ExecutionContextLike) => Promise<TResult>;
1761
1817
 
1762
- 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 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 };
1818
+ 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) {
655
621
  try {
656
- const label = await fn(env, id);
622
+ const label = await fn(pinned, id);
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(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) : 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(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(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) {
@@ -740,15 +768,17 @@ async function runUndoApply(args, opts) {
740
768
  }
741
769
  await opts.journal.dropBefore(ref);
742
770
  const recreated = spec.fidelity === "recreated";
771
+ const notRestored = result.notRestored ?? spec.notRestored;
743
772
  return {
744
773
  ok: true,
774
+ ...result.extra ?? {},
745
775
  resource: spec.resource,
746
776
  original_id: row.original_id,
747
777
  connection: row.connection,
748
778
  new_id: result.newId ?? null,
749
779
  fidelity: spec.fidelity,
750
- ...spec.notRestored ? { not_restored: spec.notRestored } : {},
751
- 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.${spec.notRestored ? ` Not restored: ${spec.notRestored}.` : ""} Report this as a replacement, not as an undo.` : `Restored ${spec.resource} ${row.original_id} to its saved values.${spec.notRestored ? ` Not restored: ${spec.notRestored}.` : ""} Anything changed since the capture was overwritten.`
780
+ ...notRestored ? { not_restored: notRestored } : {},
781
+ 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.`
752
782
  };
753
783
  }
754
784
 
@@ -935,6 +965,7 @@ export {
935
965
  decodeState,
936
966
  deleteAppFile,
937
967
  describeMissingBridge,
968
+ dispatcherApproval,
938
969
  emitMarketplaceEvent,
939
970
  encodeState,
940
971
  fetchFileAsBase64,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprigr/apps-app-sdk",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",