@sprigr/apps-app-sdk 0.8.5 → 0.8.7

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/dist/index.d.ts CHANGED
@@ -1424,6 +1424,122 @@ interface OfferUndoArgs {
1424
1424
  */
1425
1425
  declare function offerUndo(args: OfferUndoArgs): Promise<AppUndoEnvelope | undefined>;
1426
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
+
1492
+ /**
1493
+ * Derive a confirmation policy from an action registry by NAME, for apps
1494
+ * whose registry carries no destructive flag: the verb prefix says what an
1495
+ * action does (`delete_`, `void_`, `cancel_`, `archive_`), so the policy
1496
+ * follows from a small table of verb rules instead of a hand-kept list
1497
+ * that drifts the moment an action is added.
1498
+ *
1499
+ * Each rule names a prefix (or regex), the tier it lands in, and how to
1500
+ * phrase the describe from the rest of the name. The first matching rule
1501
+ * wins, so put the specific ones first.
1502
+ */
1503
+
1504
+ interface VerbRule {
1505
+ /** A name prefix (`'delete_'`) or a regex tested against the whole name. */
1506
+ match: string | RegExp;
1507
+ /** `irreversible` marks the card "cannot be undone"; `always` is a plain confirmation. */
1508
+ tier: 'irreversible' | 'always';
1509
+ /**
1510
+ * Describe for a matching action. Receives the full name and `rest`: the
1511
+ * name with the matched prefix removed and underscores turned to spaces
1512
+ * (for a regex match, the whole name spaced). No trailing period.
1513
+ */
1514
+ describe: (name: string, rest: string) => string;
1515
+ }
1516
+ interface DerivePolicyOptions {
1517
+ /** Verb rules, most specific first. */
1518
+ rules: readonly VerbRule[];
1519
+ /** Actions to leave ungated even though a rule matches (returned in `ungated`). */
1520
+ exempt?: readonly string[];
1521
+ /** Explicit rules that win over the verb table (e.g. a hand-written describe). */
1522
+ overrides?: Readonly<Record<string, ConfirmRule>>;
1523
+ }
1524
+ interface DerivedPolicy {
1525
+ policy: ConfirmationPolicy & {
1526
+ actions: Record<string, ConfirmRule>;
1527
+ };
1528
+ /** Registry names a rule matched but `exempt` kept ungated. */
1529
+ ungated: string[];
1530
+ /** Registry names no rule matched (reads and writes the table does not classify). */
1531
+ unmatched: string[];
1532
+ }
1533
+ /** Build a policy over `registry` from the verb table; see the file comment. */
1534
+ declare function deriveConfirmationPolicy(registry: Iterable<string>, opts: DerivePolicyOptions): DerivedPolicy;
1535
+ /**
1536
+ * The verb table most registries need: hard deletes, voids, refunds and
1537
+ * cancellations cannot be undone; archives, removals from a parent and
1538
+ * unassignments are plain confirmations. `subject` names the system for
1539
+ * the describe ("in Xero").
1540
+ */
1541
+ declare function commonDestructiveVerbs(subject: string): VerbRule[];
1542
+
1427
1543
  /**
1428
1544
  * @sprigr/apps-app-sdk
1429
1545
  *
@@ -1824,4 +1940,4 @@ interface ExecutionContextLike {
1824
1940
  }
1825
1941
  type HandlerFn<TArgs, TResult> = (args: TArgs, env: Record<string, unknown>, ctx?: ExecutionContextLike) => Promise<TResult>;
1826
1942
 
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 };
1943
+ 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 DerivePolicyOptions, type DerivedPolicy, 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 VerbRule, type WebhookArgs, type WfpBridgeEnv, type WfpEmitReply, actorKey, actorTool, appFileUrl, applyConfirmationPolicies, approvalHash, archiveOfferRefusal, base64ToBytes, buildConfirmationPolicy, buildMarketplaceWebhookUrl, bytesToBase64, canEmit, checkConfirmationPolicy, commonDestructiveVerbs, constantTimeEqual, createMarketplaceEmitter, createToolWrappers, decodeState, deleteAppFile, deriveConfirmationPolicy, 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
@@ -940,6 +940,98 @@ function checkConfirmationPolicy(input) {
940
940
  }
941
941
  return out.sort();
942
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
+ }
985
+
986
+ // src/write-protection/derive-policy.ts
987
+ function spaced(s) {
988
+ return s.replace(/_/g, " ");
989
+ }
990
+ function matchRule(name, rule) {
991
+ if (typeof rule.match === "string") {
992
+ return name.startsWith(rule.match) ? spaced(name.slice(rule.match.length)) : null;
993
+ }
994
+ return rule.match.test(name) ? spaced(name) : null;
995
+ }
996
+ function deriveConfirmationPolicy(registry, opts) {
997
+ const exempt = new Set(opts.exempt ?? []);
998
+ const irreversible = {};
999
+ const always = {};
1000
+ const ungated = [];
1001
+ const unmatched = [];
1002
+ const overrides = opts.overrides ?? {};
1003
+ for (const name of registry) {
1004
+ if (overrides[name]) continue;
1005
+ let hit = false;
1006
+ for (const rule of opts.rules) {
1007
+ const rest = matchRule(name, rule);
1008
+ if (rest == null) continue;
1009
+ hit = true;
1010
+ if (exempt.has(name)) ungated.push(name);
1011
+ else (rule.tier === "irreversible" ? irreversible : always)[name] = rule.describe(name, rest);
1012
+ break;
1013
+ }
1014
+ if (!hit) unmatched.push(name);
1015
+ }
1016
+ const policy = buildConfirmationPolicy({ irreversible, always, rules: overrides });
1017
+ return { policy, ungated, unmatched };
1018
+ }
1019
+ function commonDestructiveVerbs(subject) {
1020
+ const where = subject ? ` ${subject}` : "";
1021
+ return [
1022
+ { match: "delete_", tier: "irreversible", describe: (_n, rest) => `Delete ${rest}${where}` },
1023
+ { match: "void_", tier: "irreversible", describe: (_n, rest) => `Void ${rest}${where}` },
1024
+ { match: "refund_", tier: "irreversible", describe: (_n, rest) => `Refund ${rest}${where}` },
1025
+ { match: "cancel_", tier: "irreversible", describe: (_n, rest) => `Cancel ${rest}${where}` },
1026
+ { match: "destroy_", tier: "irreversible", describe: (_n, rest) => `Destroy ${rest}${where}` },
1027
+ { match: "purge_", tier: "irreversible", describe: (_n, rest) => `Purge ${rest}${where}` },
1028
+ { match: "revoke_", tier: "irreversible", describe: (_n, rest) => `Revoke ${rest}${where}` },
1029
+ { match: "archive_", tier: "always", describe: (_n, rest) => `Archive ${rest}${where}` },
1030
+ { match: "remove_", tier: "always", describe: (_n, rest) => `Remove ${rest}${where}` },
1031
+ { match: "unassign_", tier: "always", describe: (_n, rest) => `Unassign ${rest}${where}` },
1032
+ { match: "deactivate_", tier: "always", describe: (_n, rest) => `Deactivate ${rest}${where}` }
1033
+ ];
1034
+ }
943
1035
  export {
944
1036
  APPROVAL_GRANTED_KEY,
945
1037
  DEFAULT_EMIT_TIMEOUT_MS,
@@ -952,6 +1044,7 @@ export {
952
1044
  actorKey,
953
1045
  actorTool,
954
1046
  appFileUrl,
1047
+ applyConfirmationPolicies,
955
1048
  approvalHash,
956
1049
  archiveOfferRefusal,
957
1050
  base64ToBytes,
@@ -960,11 +1053,13 @@ export {
960
1053
  bytesToBase64,
961
1054
  canEmit,
962
1055
  checkConfirmationPolicy,
1056
+ commonDestructiveVerbs,
963
1057
  constantTimeEqual,
964
1058
  createMarketplaceEmitter,
965
1059
  createToolWrappers,
966
1060
  decodeState,
967
1061
  deleteAppFile,
1062
+ deriveConfirmationPolicy,
968
1063
  describeMissingBridge,
969
1064
  dispatcherApproval,
970
1065
  emitMarketplaceEvent,
@@ -979,6 +1074,7 @@ export {
979
1074
  isNotConnectedError,
980
1075
  isStagingHost,
981
1076
  listAppFiles,
1077
+ manifestIsFresh,
982
1078
  offerUndo,
983
1079
  overlaySprigr,
984
1080
  ownerFromActorKey,
@@ -996,6 +1092,7 @@ export {
996
1092
  runUndoApply,
997
1093
  safeCapture,
998
1094
  seq,
1095
+ serializeManifest,
999
1096
  set,
1000
1097
  tool,
1001
1098
  undoEnvelope,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprigr/apps-app-sdk",
3
- "version": "0.8.5",
3
+ "version": "0.8.7",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",