@sprigr/apps-app-sdk 0.8.4 → 0.8.6

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
@@ -73,7 +73,7 @@ Three platform tiers, and they are not substitutes for each other. Full guide: [
73
73
  - `undoEnvelope({...})` throws on a blank field. The platform drops the whole envelope on a blank and mints nothing, silently.
74
74
  - `refuseWithoutForce(name, handler, opts)` / `archiveOfferRefusal(...)`: a delete refuses once and offers the vendor's native archive unless `force: true`.
75
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.
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.
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).
77
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.
78
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`.
79
79
 
package/dist/index.d.ts CHANGED
@@ -1153,9 +1153,11 @@ interface ApprovalGateOptions<E, P = E> {
1153
1153
  /**
1154
1154
  * Human label for a raw id, looked up THROUGH the pinned connection. Fall
1155
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.
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).
1157
1159
  */
1158
- describeTarget?: (pinned: P, id: string) => Promise<string>;
1160
+ describeTarget?: (pinned: P, id: string, name: string) => Promise<string>;
1159
1161
  /**
1160
1162
  * Where before-images go. Required when any spec declares `undo`. The
1161
1163
  * journal's own capture returns null rather than throwing, and this wrapper
@@ -1269,6 +1271,13 @@ interface RestoreResult {
1269
1271
  error?: string;
1270
1272
  notRestored?: string;
1271
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;
1272
1281
  }
1273
1282
  interface JournalRowLike<T = unknown> {
1274
1283
  entity: string;
@@ -1415,6 +1424,71 @@ interface OfferUndoArgs {
1415
1424
  */
1416
1425
  declare function offerUndo(args: OfferUndoArgs): Promise<AppUndoEnvelope | undefined>;
1417
1426
 
1427
+ /**
1428
+ * Apply confirmation policies to a parsed `sprigr-app.json`, for an app's
1429
+ * `gen:confirmation` script and its drift-guard test.
1430
+ *
1431
+ * Pure: takes the parsed manifest, mutates it in place, returns what it
1432
+ * touched. The script owns the two lines of file I/O around it, so this
1433
+ * module stays free of `node:fs` and safe to bundle into a Worker.
1434
+ *
1435
+ * Besides writing each policy, it retires the two mechanisms a policy
1436
+ * supersedes: the legacy `confirmation_required` flag (a bare
1437
+ * `{ always: true }` with no describe) and any app-declared `confirm` input
1438
+ * (the platform injects its own top-level `confirm` wherever a policy is
1439
+ * present). Both are stripped from EVERY tool, not only the gated ones,
1440
+ * because a stray copy on an ungated tool is how a handler ends up reading
1441
+ * a flag nothing enforces.
1442
+ */
1443
+
1444
+ interface ManifestDispatchBlock {
1445
+ actionField: string;
1446
+ actions: string[];
1447
+ actionInputs?: Record<string, {
1448
+ accepts: string[];
1449
+ required?: string[];
1450
+ }>;
1451
+ }
1452
+ interface ManifestToolLike {
1453
+ name: string;
1454
+ input_schema?: {
1455
+ properties?: Record<string, unknown>;
1456
+ required?: string[];
1457
+ [k: string]: unknown;
1458
+ };
1459
+ confirmation?: unknown;
1460
+ confirmation_required?: boolean;
1461
+ dispatch?: unknown;
1462
+ [k: string]: unknown;
1463
+ }
1464
+ interface ManifestLike {
1465
+ tools?: ManifestToolLike[];
1466
+ [k: string]: unknown;
1467
+ }
1468
+ interface ApplyPoliciesOptions {
1469
+ /** Tool name -> its policy: a tool-level rule, or `{ actions }` for a dispatcher. */
1470
+ policies: Readonly<Record<string, ConfirmationPolicy>>;
1471
+ /** Tool name -> dispatch block to write alongside (dispatchers only). */
1472
+ dispatch?: Readonly<Record<string, ManifestDispatchBlock>>;
1473
+ /** Delete `confirmation_required` from every tool. Default true. */
1474
+ stripLegacyFlag?: boolean;
1475
+ /** Delete an app-declared `confirm` input (property + required entry) from every tool. Default true. */
1476
+ dropConfirmParam?: boolean;
1477
+ }
1478
+ /**
1479
+ * Write the policies (and dispatch blocks) onto the named tools in place.
1480
+ * Throws when a policy names a tool the manifest does not have, so a
1481
+ * renamed tool cannot silently lose its gate. Returns the tool names touched.
1482
+ */
1483
+ declare function applyConfirmationPolicies(manifest: ManifestLike, opts: ApplyPoliciesOptions): string[];
1484
+ /**
1485
+ * True when applying the policies to a copy of the manifest changes nothing:
1486
+ * the committed file is a fresh generation. The drift-guard test's one call.
1487
+ */
1488
+ declare function manifestIsFresh(manifest: ManifestLike, opts: ApplyPoliciesOptions): boolean;
1489
+ /** The manifest, serialised the way the repo commits it (2-space, trailing newline). */
1490
+ declare function serializeManifest(manifest: ManifestLike): string;
1491
+
1418
1492
  /**
1419
1493
  * @sprigr/apps-app-sdk
1420
1494
  *
@@ -1815,4 +1889,4 @@ interface ExecutionContextLike {
1815
1889
  }
1816
1890
  type HandlerFn<TArgs, TResult> = (args: TArgs, env: Record<string, unknown>, ctx?: ExecutionContextLike) => Promise<TResult>;
1817
1891
 
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 };
1892
+ export { APPROVAL_GRANTED_KEY, type Actor, type AppApprovalEnvelope, type AppFileListItem, type AppFileUrlArgs, type AppFileUrlResult, type AppFilesEnv, type AppUndoEnvelope, type ApplyPoliciesOptions, 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 ManifestDispatchBlock, type ManifestLike, type ManifestToolLike, 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, applyConfirmationPolicies, 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, manifestIsFresh, offerUndo, overlaySprigr, ownerFromActorKey, parseActor, putAppFile, putAppFileStream, randomHex, recreatedWarning, refuseWithoutForce, requireApproval, resolveInstallBridge, resolveOAuthBouncerBase, resolveOAuthBouncerCallbackUrl, resolvePlatformWebhookBase, runUndoApply, safeCapture, seq, serializeManifest, set, tool, undoEnvelope, withSprigrEmitFallback };
package/dist/index.js CHANGED
@@ -617,9 +617,9 @@ function rawIdOf(args, keys) {
617
617
  function isOk(result) {
618
618
  return !!result && typeof result === "object" && result.ok !== false;
619
619
  }
620
- async function safeLabel(fn, pinned, id) {
620
+ async function safeLabel(fn, pinned, id, name) {
621
621
  try {
622
- const label = await fn(pinned, id);
622
+ const label = await fn(pinned, id, name);
623
623
  return label && label.trim() ? label : id;
624
624
  } catch {
625
625
  return id;
@@ -629,11 +629,11 @@ async function pin(opts, env, connection) {
629
629
  if (!connection || !opts.pinEnv) return env;
630
630
  return opts.pinEnv(env, connection);
631
631
  }
632
- async function askPass(spec, params, env, opts, hashPrefix) {
632
+ async function askPass(name, spec, params, env, opts, hashPrefix) {
633
633
  const connection = await opts.resolveConnection(env, params);
634
634
  const rawId = rawIdOf(params, spec.keys);
635
635
  const pinned = await pin(opts, env, connection);
636
- const target = rawId && opts.describeTarget ? await safeLabel(opts.describeTarget, pinned, rawId) : rawId || "(unknown id)";
636
+ const target = rawId && opts.describeTarget ? await safeLabel(opts.describeTarget, pinned, rawId, name) : rawId || "(unknown id)";
637
637
  const extra = spec.hash ? spec.hash(params) : [];
638
638
  return {
639
639
  ok: false,
@@ -688,7 +688,7 @@ function requireApproval(handlers, specs, opts) {
688
688
  const { [APPROVAL_GRANTED_KEY]: _granted, ...rest } = a;
689
689
  return grantedPass(name, spec, rest, env, opts, () => inner(rest, env, ctx));
690
690
  }
691
- return askPass(spec, a, env, opts, []);
691
+ return askPass(name, spec, a, env, opts, []);
692
692
  };
693
693
  }
694
694
  return gated;
@@ -707,7 +707,7 @@ function dispatcherApproval(specs, opts) {
707
707
  if (a[APPROVAL_GRANTED_KEY] === true) {
708
708
  return grantedPass(action, spec, params, env, opts, write);
709
709
  }
710
- return askPass(spec, params, env, opts, [action]);
710
+ return askPass(action, spec, params, env, opts, [action]);
711
711
  }
712
712
  };
713
713
  }
@@ -733,7 +733,7 @@ async function runUndoApply(args, opts) {
733
733
  };
734
734
  }
735
735
  const before = row.before;
736
- if (!before || typeof before !== "object") {
736
+ if (!before || typeof before !== "object" || Array.isArray(before)) {
737
737
  return { ok: false, error: "before_image_unreadable", note: "The saved copy could not be read back. Nothing was changed." };
738
738
  }
739
739
  let pinned;
@@ -769,6 +769,7 @@ async function runUndoApply(args, opts) {
769
769
  await opts.journal.dropBefore(ref);
770
770
  const recreated = spec.fidelity === "recreated";
771
771
  const notRestored = result.notRestored ?? spec.notRestored;
772
+ const suffix = result.note && result.note.trim() ? ` ${result.note.trim()}` : "";
772
773
  return {
773
774
  ok: true,
774
775
  ...result.extra ?? {},
@@ -778,7 +779,7 @@ async function runUndoApply(args, opts) {
778
779
  new_id: result.newId ?? null,
779
780
  fidelity: spec.fidelity,
780
781
  ...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.`
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}`
782
783
  };
783
784
  }
784
785
 
@@ -939,6 +940,48 @@ function checkConfirmationPolicy(input) {
939
940
  }
940
941
  return out.sort();
941
942
  }
943
+
944
+ // src/write-protection/manifest.ts
945
+ function applyConfirmationPolicies(manifest, opts) {
946
+ const tools = manifest.tools ?? [];
947
+ const byName = new Map(tools.map((t) => [t.name, t]));
948
+ const touched = [];
949
+ for (const [name, policy] of Object.entries(opts.policies)) {
950
+ const tool2 = byName.get(name);
951
+ if (!tool2) throw new Error(`applyConfirmationPolicies: no tool named '${name}' in the manifest`);
952
+ tool2.confirmation = policy;
953
+ touched.push(name);
954
+ }
955
+ for (const [name, block] of Object.entries(opts.dispatch ?? {})) {
956
+ const tool2 = byName.get(name);
957
+ if (!tool2) throw new Error(`applyConfirmationPolicies: no tool named '${name}' in the manifest (dispatch)`);
958
+ tool2.dispatch = block;
959
+ if (!touched.includes(name)) touched.push(name);
960
+ }
961
+ const stripLegacy = opts.stripLegacyFlag ?? true;
962
+ const dropConfirm = opts.dropConfirmParam ?? true;
963
+ for (const tool2 of tools) {
964
+ if (stripLegacy) delete tool2.confirmation_required;
965
+ if (dropConfirm) {
966
+ const schema = tool2.input_schema;
967
+ if (schema?.properties && "confirm" in schema.properties) delete schema.properties.confirm;
968
+ if (schema?.required?.includes("confirm")) {
969
+ schema.required = schema.required.filter((k) => k !== "confirm");
970
+ if (schema.required.length === 0) delete schema.required;
971
+ }
972
+ }
973
+ }
974
+ return touched;
975
+ }
976
+ function manifestIsFresh(manifest, opts) {
977
+ const copy = JSON.parse(JSON.stringify(manifest));
978
+ applyConfirmationPolicies(copy, opts);
979
+ return JSON.stringify(copy) === JSON.stringify(manifest);
980
+ }
981
+ function serializeManifest(manifest) {
982
+ return `${JSON.stringify(manifest, null, 2)}
983
+ `;
984
+ }
942
985
  export {
943
986
  APPROVAL_GRANTED_KEY,
944
987
  DEFAULT_EMIT_TIMEOUT_MS,
@@ -951,6 +994,7 @@ export {
951
994
  actorKey,
952
995
  actorTool,
953
996
  appFileUrl,
997
+ applyConfirmationPolicies,
954
998
  approvalHash,
955
999
  archiveOfferRefusal,
956
1000
  base64ToBytes,
@@ -978,6 +1022,7 @@ export {
978
1022
  isNotConnectedError,
979
1023
  isStagingHost,
980
1024
  listAppFiles,
1025
+ manifestIsFresh,
981
1026
  offerUndo,
982
1027
  overlaySprigr,
983
1028
  ownerFromActorKey,
@@ -995,6 +1040,7 @@ export {
995
1040
  runUndoApply,
996
1041
  safeCapture,
997
1042
  seq,
1043
+ serializeManifest,
998
1044
  set,
999
1045
  tool,
1000
1046
  undoEnvelope,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprigr/apps-app-sdk",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",