@sprigr/apps-app-sdk 0.8.0 → 0.8.1

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
@@ -72,7 +72,8 @@ Three platform tiers, and they are not substitutes for each other. Full guide: [
72
72
  - `undoEnvelope({...})` throws on a blank field. The platform drops the whole envelope on a blank and mints nothing, silently.
73
73
  - `refuseWithoutForce(name, handler, opts)` / `archiveOfferRefusal(...)`: a delete refuses once and offers the vendor's native archive unless `force: true`.
74
74
  - `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.
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
+ - `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.
76
77
  - `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`.
77
78
 
78
79
  **Never offer `_undo` for money or messages.** Refunds, captures, cancellations, sent mail: the approval gate is the only control there.
package/dist/index.d.ts CHANGED
@@ -1190,19 +1190,28 @@ declare function requireApproval<E>(handlers: Record<string, ToolHandler<E>>, sp
1190
1190
  * `pin`, a replay lands on the install's CURRENT default and rebuilds the
1191
1191
  * object in the wrong customer account, and nothing errors.
1192
1192
  *
1193
+ * `E` is the env the tool receives; `P` is what `pin` hands to `restore`. For
1194
+ * a store-pinned env they are the same type (Shopify). For an app that
1195
+ * re-authenticates per actor and organisation, `P` is the fresh token state
1196
+ * (Xero), which is why they are separate parameters.
1197
+ *
1193
1198
  * Extracted from the Shopify and Xero `undo-apply.ts` handlers, which were
1194
1199
  * byte-similar apart from their restore call and their pin.
1195
1200
  */
1196
1201
 
1197
- interface RestoreSpec<E, T = Record<string, unknown>> {
1202
+ interface RestoreSpec<P, T = Record<string, unknown>> {
1198
1203
  resource: string;
1199
1204
  fidelity: UndoFidelity;
1200
- /** Rebuild or write back, through the PINNED env. */
1201
- restore: (pinnedEnv: E, before: T, row: JournalRowLike<T>) => Promise<{
1205
+ /**
1206
+ * Rebuild or write back, through the PINNED connection. May return
1207
+ * `{ ok: false, error }` or throw; both become `restore_failed` and the
1208
+ * before-image is kept.
1209
+ */
1210
+ restore: (pinned: P, before: T, row: JournalRowLike<T>) => Promise<{
1202
1211
  ok: boolean;
1203
1212
  newId?: string;
1204
1213
  error?: string;
1205
- }>;
1214
+ } | void>;
1206
1215
  /** What the restore does NOT bring back, folded into the note verbatim. */
1207
1216
  notRestored?: string;
1208
1217
  }
@@ -1212,19 +1221,20 @@ interface JournalRowLike<T = unknown> {
1212
1221
  connection: string | null;
1213
1222
  before: T;
1214
1223
  }
1215
- interface UndoApplyOptions<E> {
1224
+ interface UndoApplyOptions<E, P = E> {
1216
1225
  env: E;
1217
1226
  journal: {
1218
1227
  loadBefore<T = unknown>(ref: string): Promise<JournalRowLike<T> | null>;
1219
1228
  dropBefore(ref: string): Promise<void>;
1220
1229
  };
1221
- specs: Record<string, RestoreSpec<E>>;
1230
+ specs: Record<string, RestoreSpec<P>>;
1222
1231
  /**
1223
- * Re-pin the env to the journalled connection. May be async (re-auth per
1224
- * actor, as Xero does) and may throw or return null to REFUSE, e.g. a
1225
- * session-stateful app whose current business differs from the row's.
1232
+ * Re-pin to the journalled connection. May be async (re-auth per actor, as
1233
+ * Xero does). Throw to report `connection_unavailable`; return null to
1234
+ * REFUSE with `connection_mismatch`, e.g. a session-stateful app whose
1235
+ * current business differs from the row's.
1226
1236
  */
1227
- pin: (env: E, connection: string | null, args: ToolArgs) => Promise<E | null> | E | null;
1237
+ pin: (env: E, connection: string | null, args: ToolArgs) => Promise<P | null> | P | null;
1228
1238
  }
1229
1239
  type UndoApplyResult = {
1230
1240
  ok: true;
@@ -1241,7 +1251,7 @@ type UndoApplyResult = {
1241
1251
  note: string;
1242
1252
  };
1243
1253
  /** Run the reversal. Every failure names what it was and states that nothing changed. */
1244
- declare function runUndoApply<E>(args: ToolArgs, opts: UndoApplyOptions<E>): Promise<UndoApplyResult>;
1254
+ declare function runUndoApply<E, P = E>(args: ToolArgs, opts: UndoApplyOptions<E, P>): Promise<UndoApplyResult>;
1245
1255
 
1246
1256
  /**
1247
1257
  * Declare a dispatcher tool's confirmation policy beside its action registry,
@@ -1300,6 +1310,55 @@ interface PolicyCheckInput {
1300
1310
  */
1301
1311
  declare function checkConfirmationPolicy(input: PolicyCheckInput): string[];
1302
1312
 
1313
+ /**
1314
+ * The two halves of offering an undo, for handlers that are not behind an
1315
+ * approval card (a dispatcher's `update_*` actions, for instance). The order
1316
+ * is the whole contract:
1317
+ *
1318
+ * const before = await safeCapture(scope, action, id, () => read(pinned, id));
1319
+ * const result = await write(pinned, input); // happens regardless
1320
+ * const _undo = await offerUndo({ journal, entity: action, id, before, connection, ... });
1321
+ * return { ok: true, result, ...(_undo ? { _undo } : {}) };
1322
+ *
1323
+ * `requireApproval` uses the same two functions on its granted pass.
1324
+ */
1325
+ /**
1326
+ * Read the before-image, degrading to null on any failure. A capture that
1327
+ * throws (404, scope denied, network) is not a reason to refuse a write the
1328
+ * user asked for; it is a reason to offer no undo, and to say so in the log.
1329
+ */
1330
+ declare function safeCapture(scope: string, name: string, id: string, capture: () => Promise<unknown>): Promise<Record<string, unknown> | null>;
1331
+ interface OfferUndoArgs {
1332
+ journal: CaptureJournal;
1333
+ /** The tool or action that captured; the reverse tool looks the spec up by it. */
1334
+ entity: string;
1335
+ id: string;
1336
+ /** From `safeCapture`. Null means no undo is offered, deliberately. */
1337
+ before: Record<string, unknown> | null;
1338
+ /** The resolved store / organisation / business, or null for a single-connection app. */
1339
+ connection: string | null;
1340
+ fidelity: UndoFidelity;
1341
+ resource: string;
1342
+ /** How the change reads in `list_undoable_changes`; the connection is appended when present. */
1343
+ describe: (before: Record<string, unknown>, id: string) => string;
1344
+ /** Relayed verbatim; never empty. */
1345
+ warning: string;
1346
+ /** Word between the description and the connection: "on" for a store, "in" for an organisation. */
1347
+ connectionPreposition?: 'on' | 'in';
1348
+ /**
1349
+ * What `describes` calls the connection, when the journalled id is not
1350
+ * readable by a person (a Xero tenantId). Defaults to `connection`. The
1351
+ * journal row always stores `connection` itself, because that is the pin.
1352
+ */
1353
+ connectionLabel?: string;
1354
+ }
1355
+ /**
1356
+ * Store the before-image and build the `_undo` envelope, or return undefined
1357
+ * when there is nothing to offer. Never returns an envelope for a null capture
1358
+ * or a journal that could not store the copy.
1359
+ */
1360
+ declare function offerUndo(args: OfferUndoArgs): Promise<AppUndoEnvelope | undefined>;
1361
+
1303
1362
  /**
1304
1363
  * @sprigr/apps-app-sdk
1305
1364
  *
@@ -1700,4 +1759,4 @@ interface ExecutionContextLike {
1700
1759
  }
1701
1760
  type HandlerFn<TArgs, TResult> = (args: TArgs, env: Record<string, unknown>, ctx?: ExecutionContextLike) => Promise<TResult>;
1702
1761
 
1703
- 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 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, overlaySprigr, ownerFromActorKey, parseActor, putAppFile, putAppFileStream, randomHex, recreatedWarning, refuseWithoutForce, requireApproval, resolveInstallBridge, resolveOAuthBouncerBase, resolveOAuthBouncerCallbackUrl, resolvePlatformWebhookBase, runUndoApply, seq, set, tool, undoEnvelope, withSprigrEmitFallback };
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 };
package/dist/index.js CHANGED
@@ -575,6 +575,37 @@ function refuseWithoutForce(deleteAction, inner, opts) {
575
575
  };
576
576
  }
577
577
 
578
+ // src/write-protection/undo-capture.ts
579
+ async function safeCapture(scope, name, id, capture) {
580
+ try {
581
+ const got = await capture();
582
+ return got && typeof got === "object" ? got : null;
583
+ } catch (err) {
584
+ console.warn(`[${scope}] ${name} ${id}: capture failed, writing without undo: ${String(err)}`);
585
+ return null;
586
+ }
587
+ }
588
+ async function offerUndo(args) {
589
+ if (!args.before) return void 0;
590
+ const captured = await args.journal.captureBefore({
591
+ entity: args.entity,
592
+ originalId: args.id,
593
+ before: args.before,
594
+ connection: args.connection
595
+ });
596
+ if (!captured) return void 0;
597
+ const base = args.describe(args.before, args.id);
598
+ const prep = args.connectionPreposition ?? "on";
599
+ const label = args.connectionLabel ?? args.connection;
600
+ return undoEnvelope({
601
+ fidelity: args.fidelity,
602
+ warning: args.warning,
603
+ describes: label ? `${base} ${prep} ${label}` : base,
604
+ resource: args.resource,
605
+ ref: captured.ref
606
+ });
607
+ }
608
+
578
609
  // src/write-protection/require-approval.ts
579
610
  function rawIdOf(args, keys) {
580
611
  for (const k of keys) {
@@ -635,33 +666,22 @@ async function granted(name, spec, args, env, ctx, inner, opts) {
635
666
  const id = rawIdOf(args, spec.keys);
636
667
  if (!undo || !id) return stamp(await inner(args, env, ctx));
637
668
  const pinned = connection ? opts.pinEnv(env, connection) : env;
638
- let before = null;
639
- try {
640
- const got = await undo.capture(pinned, id, args);
641
- before = got && typeof got === "object" ? got : null;
642
- } catch (err) {
643
- console.warn(`[${opts.scope}] ${name} ${id}: capture failed, writing without undo: ${String(err)}`);
644
- }
669
+ const before = await safeCapture(opts.scope, name, id, () => undo.capture(pinned, id, args));
645
670
  const result = await inner(args, env, ctx);
646
671
  if (!isOk(result) || !before) return stamp(result);
647
- const captured = await opts.journal(env).captureBefore({
672
+ const envelope = await offerUndo({
673
+ journal: opts.journal(env),
648
674
  entity: name,
649
- originalId: id,
675
+ id,
650
676
  before,
651
- connection: connection || null
677
+ connection: connection || null,
678
+ fidelity: undo.fidelity,
679
+ resource: undo.resource,
680
+ describe: undo.describe,
681
+ warning: undo.warning(id, before)
652
682
  });
653
- if (!captured) return stamp(result);
654
- const describes = connection ? `${undo.describe(before, id)} on ${connection}` : undo.describe(before, id);
655
- return {
656
- ...stamp(result),
657
- _undo: undoEnvelope({
658
- fidelity: undo.fidelity,
659
- warning: undo.warning(id, before),
660
- describes,
661
- resource: undo.resource,
662
- ref: captured.ref
663
- })
664
- };
683
+ if (!envelope) return stamp(result);
684
+ return { ...stamp(result), _undo: envelope };
665
685
  }
666
686
 
667
687
  // src/write-protection/undo-apply.ts
@@ -698,14 +718,19 @@ async function runUndoApply(args, opts) {
698
718
  note: `Could not reach the connection this change was made in${row.connection ? ` (${row.connection})` : ""}: ${String(err)}. Nothing was changed.`
699
719
  };
700
720
  }
701
- if (!pinned) {
721
+ if (pinned === null || pinned === void 0) {
702
722
  return {
703
723
  ok: false,
704
724
  error: "connection_mismatch",
705
725
  note: `This change was made in ${row.connection ?? "a different connection"}, which is not the one currently selected. Switch to it and retry. Nothing was changed.`
706
726
  };
707
727
  }
708
- const result = await spec.restore(pinned, before, row);
728
+ let result;
729
+ try {
730
+ result = await spec.restore(pinned, before, row) ?? { ok: true };
731
+ } catch (err) {
732
+ result = { ok: false, error: String(err) };
733
+ }
709
734
  if (!result.ok) {
710
735
  return {
711
736
  ok: false,
@@ -922,6 +947,7 @@ export {
922
947
  isNotConnectedError,
923
948
  isStagingHost,
924
949
  listAppFiles,
950
+ offerUndo,
925
951
  overlaySprigr,
926
952
  ownerFromActorKey,
927
953
  parseActor,
@@ -936,6 +962,7 @@ export {
936
962
  resolveOAuthBouncerCallbackUrl,
937
963
  resolvePlatformWebhookBase,
938
964
  runUndoApply,
965
+ safeCapture,
939
966
  seq,
940
967
  set,
941
968
  tool,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprigr/apps-app-sdk",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",