@sprigr/apps-app-sdk 0.18.0 → 0.20.0

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
@@ -15,7 +15,7 @@ The runtime injects the `env.SPRIGR` host object **only on `/__sprigr/*` dispatc
15
15
  - `canEmit(env)` — whether an emit could reach the platform by either transport. Gate work that only exists to feed an emit on this, not on `env.SPRIGR?.emit`.
16
16
  - `withSprigrEmitFallback(env)` — repair `env.SPRIGR.emit` once and leave existing call sites untouched. Matches the host object's contract (resolves `{ ok, eventId, queued }`, throws on non-2xx).
17
17
  - `logToPlatform(env, entry | entry[], opts?)`: durable app logging from either context (`env.SPRIGR.log` on dispatch, `POST ${SPRIGR_PLATFORM_BASE}/internal/wfp/log` from an inline route). Rows land in the platform's `system_logs` Analytics Engine dataset (90-day retention, scoped to the install's company, category prefixed with the app slug), so use it instead of a per-webhook or per-tick audit row in your D1: D1 bills every row written, AE does not. Validation throws synchronously on a cap violation (summary 256, detail 4096, metadata JSON 3840, category 64 matching `[A-Za-z0-9._:-]`, 50 entries per call; nothing is ever truncated); after that the promise never rejects and resolves `{ ok: true, written }` or `{ ok: false, error, status?, detail? }`. Pass `{ waitUntil: ctx.waitUntil.bind(ctx) }` on an inline route so the send outlives the response. `withSprigrLogFallback(env, opts?)` installs `env.SPRIGR.log` once for many call sites; `canLog(env)` gates work whose only purpose is a log row; the `SprigrLogEntry` / `SprigrLogFn` types let your env interface declare the host member.
18
- - `partialUpdateData(env, objects, opts?)`: merge fields into objects already in the app's data index without reading them first (`env.SPRIGR.data.partialUpdate` on dispatch, `POST ${SPRIGR_PLATFORM_BASE}/internal/wfp/data/partial-update` from an inline route). Deep merge: nested objects recurse, arrays and scalars replace wholesale, absent keys survive, `null` overwrites. `createIfNotExists` defaults to `false`; on a sharded logical index every patch must carry the `shard_field` value or the batch fails with `shard_field_invalid`; 1000 objects per call like `import`. Throws on a bad batch before sending (`SprigrDataValidationError`) and on a platform rejection (`.status`, `.error`, `.detail` on the `Error`), the same contract as `import`. `canPartialUpdate(env)` gates a patch step on either transport; the `SprigrDataPartialUpdateOpts` / `SprigrDataPartialUpdateResult` types let your env interface declare the host member.
18
+ - `partialUpdateData(env, objects, opts?)`: merge fields into objects already in the app's data index without reading them first (`env.SPRIGR.data.partialUpdate` on dispatch, `POST ${SPRIGR_PLATFORM_BASE}/internal/wfp/data/partial-update` from an inline route). Deep merge: nested objects recurse, arrays and scalars replace wholesale, absent keys survive, `null` overwrites. `createIfNotExists` defaults to `false`; on a sharded logical index every patch must carry the `shard_field` value or the batch fails with `shard_field_invalid`; 1000 objects per call like `import`. Throws on a bad batch before sending (`SprigrDataValidationError`) and on a platform rejection (`.status`, `.error`, `.detail` on the `Error`), the same contract as `import`. With `{ withAcl: true }` it patches existing rows of the app's `-acl-files` index: each object carries `objectID` plus `acl_principals` and/or label fields the manifest declares in `data_index.acl_patchable_fields` (never `content`), and an object that omits `acl_principals` keeps its stored ones. `canPartialUpdate(env)` gates a patch step on either transport; the `SprigrDataPartialUpdateOpts` / `SprigrDataPartialUpdateResult` types let your env interface declare the host member.
19
19
  - `resolveInstallBridge(env)` / `installTokenPost(bridge, path, body, opts?)`: build your own `/internal/wfp/*` fallback (collections, files, inbox) with the auth and error extraction handled. A non-2xx throws an `Error` whose `.status`, `.error` (the platform's code) and `.detail` are set alongside the message.
20
20
  - `overlaySprigr(env, sprigr)` — overlay a patched `SPRIGR` via `Object.create`. Never rebuild a dispatch-path env by spread: the real bindings live on the prototype and `SPRIGR` is non-enumerable, so `{ ...env }` yields an env whose `DB` is `undefined`.
21
21
 
@@ -99,8 +99,9 @@ Three platform tiers, and they are not substitutes for each other. Full guide: [
99
99
  - `undoEnvelope({...})` throws on a blank field. The platform drops the whole envelope on a blank and mints nothing, silently.
100
100
  - `refuseWithoutForce(name, handler, opts)` / `archiveOfferRefusal(...)`: a delete refuses once and offers the vendor's native archive unless `force: true`.
101
101
  - `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.
102
+ - **Ask once when T1 and T2 both gate an action.** Pass `approval: { covered, conditional }` to `buildConfirmationPolicy` (or `deriveConfirmationPolicy`) and to `checkConfirmationPolicy`: `covered` is your approval spec keys (`Object.keys(APPROVAL_SPECS)`), `conditional` the covered actions your app sends through the gate only under a condition of its own (simpro's `GATE_ONLY_WHEN`), `[]` when there are none. Both are required. Each covered, unconditional, gated action gets `attended: 'approval_card'`, and the platform then skips the confirmation on a turn with a person on it, so the card is the only prompt; unattended runs still confirm. The builder never marks a conditional action, and the check fails a key that no spec covers or that sits on a conditional action (no prompt at all on a call that skips the card), a covered unconditional action without one (it asks twice), and a `conditional` entry with no spec. For flat `requireApproval` tools, run `attendedFindings(tool, rule, { covered, conditional })` over each tool-level rule. Needs a platform with sprigr-team decision 0167; an older one ignores the key and keeps asking twice.
102
103
  - `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).
103
104
  - `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.
104
- - `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`.
105
+ - `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`. It also refuses an `attended` key with any value other than `'approval_card'`, or on a rule that gates nothing (never baselined).
105
106
 
106
107
  **Never offer `_undo` for money or messages.** Refunds, captures, cancellations, sent mail: the approval gate is the only control there.
@@ -93,6 +93,33 @@ export function scanManifest(manifest) {
93
93
  return out;
94
94
  }
95
95
 
96
+ /**
97
+ * Malformed `attended` keys (sprigr-team decision 0167), as `tool` or
98
+ * `tool:action` findings. Not baselined: no app may ship one.
99
+ *
100
+ * A manifest cannot say whether an approval card really covers the action,
101
+ * because the specs are code. That half is `checkConfirmationPolicy` /
102
+ * `attendedFindings` with `approvalCovered`, in the app's own test. What a
103
+ * manifest CAN show is a key the platform would ignore (any value but
104
+ * 'approval_card', so the person is quietly asked twice) and a key on a rule
105
+ * that gates nothing (so there is no confirmation for it to step aside from,
106
+ * which means it was written by hand).
107
+ */
108
+ export function scanAttended(manifest) {
109
+ const out = [];
110
+ const check = (label, rule) => {
111
+ if (!rule || typeof rule !== 'object' || !('attended' in rule)) return;
112
+ if (rule.attended !== 'approval_card') out.push(`${label}: attended must be 'approval_card', got ${JSON.stringify(rule.attended)}`);
113
+ else if (!isGatedRule(rule)) out.push(`${label}: attended on a rule with no always or when, so it gates nothing`);
114
+ };
115
+ for (const tool of manifest.tools || []) {
116
+ if (!tool || !tool.confirmation || typeof tool.confirmation !== 'object') continue;
117
+ check(tool.name, tool.confirmation);
118
+ for (const [a, rule] of Object.entries(tool.confirmation.actions || {})) check(`${tool.name}:${a}`, rule);
119
+ }
120
+ return out;
121
+ }
122
+
96
123
  /** Apply the app's allowlist. Returns { violations, stale, unreasoned }. */
97
124
  export function applyAllowlist(violations, allowlist) {
98
125
  const allow = (allowlist && allowlist.allow) || {};
@@ -151,6 +178,7 @@ export function run(argv, cwd = process.cwd()) {
151
178
  const allowlist = existsSync(allowPath) ? readJson(allowPath) : null;
152
179
  const raw = scanManifest(manifest);
153
180
  const { violations, stale, unreasoned } = applyAllowlist(raw, allowlist);
181
+ for (const f of scanAttended(manifest)) failures.push(`${slug}: ${f}`);
154
182
  for (const k of unreasoned) failures.push(`${slug}: write-protection.json allow entry "${k}" has no reason`);
155
183
  for (const k of stale) failures.push(`${slug}: write-protection.json allow entry "${k}" is stale (no longer destructive-and-ungated); remove it`);
156
184
 
package/dist/index.d.ts CHANGED
@@ -1453,6 +1453,18 @@ interface ConfirmRule {
1453
1453
  describe?: string;
1454
1454
  /** Appends "which cannot be undone" so the model cannot undersell it. */
1455
1455
  irreversible?: boolean;
1456
+ /**
1457
+ * `'approval_card'`: this action's human-approval card (an approval spec)
1458
+ * covers it whenever a person is on the turn, so the platform skips the
1459
+ * confirmation there and the person is asked once, by the card. Unattended
1460
+ * runs still confirm. Never hand-write it: pass `approval: { covered,
1461
+ * conditional }` to `buildConfirmationPolicy` / `deriveConfirmationPolicy`
1462
+ * so it follows the specs, and to `checkConfirmationPolicy` so a stale or
1463
+ * conditionally gated key fails the test.
1464
+ * Needs a platform with sprigr-team decision 0167; an older one ignores it
1465
+ * and keeps asking twice.
1466
+ */
1467
+ attended?: 'approval_card';
1456
1468
  }
1457
1469
  interface ConfirmCondition {
1458
1470
  count: string;
@@ -2021,6 +2033,34 @@ interface PolicySource {
2021
2033
  always?: Record<string, string>;
2022
2034
  /** Full rules, for thresholds and anything the two maps above cannot express. */
2023
2035
  rules?: Record<string, ConfirmRule>;
2036
+ /**
2037
+ * Which actions an approval card covers; see {@link ApprovalCoverage}. Each
2038
+ * covered action that also has a rule here, and is not `conditional`, gets
2039
+ * `attended: 'approval_card'`, so the person is asked once on an attended
2040
+ * turn (the card) instead of twice.
2041
+ */
2042
+ approval?: ApprovalCoverage;
2043
+ }
2044
+ /**
2045
+ * Which actions an approval card covers, for deriving and checking
2046
+ * `attended: 'approval_card'` (sprigr-team decision 0167).
2047
+ *
2048
+ * Both fields are required on purpose. The SDK can see an app's approval
2049
+ * specs but not any condition the app wraps around the gate (simpro sends its
2050
+ * deletes to the gate only with `force: true`), and an action marked attended
2051
+ * whose call skips the card would skip the confirmation too: no prompt at all.
2052
+ * So every app states its conditional actions, `[]` when it has none, and the
2053
+ * builder and the check refuse to mark them.
2054
+ */
2055
+ interface ApprovalCoverage {
2056
+ /** Every action with an approval spec: the keys of the specs object you hand to the gate. */
2057
+ covered: Iterable<string>;
2058
+ /**
2059
+ * Covered actions the app sends through the gate only under a condition of
2060
+ * its own (simpro's `GATE_ONLY_WHEN`). Never marked attended; they keep the
2061
+ * confirmation. Pass `[]` when every covered action always reaches the gate.
2062
+ */
2063
+ conditional: Iterable<string>;
2024
2064
  }
2025
2065
  /** Build a sorted `{ actions }` policy. An action named in two groups throws. */
2026
2066
  declare function buildConfirmationPolicy(src: PolicySource): ConfirmationPolicy;
@@ -2053,7 +2093,23 @@ interface PolicyCheckInput {
2053
2093
  */
2054
2094
  nestedUnderInput?: boolean;
2055
2095
  moneyFields?: readonly string[];
2096
+ /**
2097
+ * Which actions an approval card covers. With it, the check fails a rule
2098
+ * carrying `attended: 'approval_card'` on an action no spec covers or that
2099
+ * the app gates conditionally (either way the key removes the person's only
2100
+ * prompt), a covered, unconditional, gated action that lacks the key (it asks
2101
+ * twice), and a `conditional` entry with no spec. Without it, any `attended`
2102
+ * key is itself a finding, because nothing can vouch for it.
2103
+ */
2104
+ approval?: ApprovalCoverage;
2056
2105
  }
2106
+ /**
2107
+ * Findings for one rule's `attended` key against the approval coverage.
2108
+ * Exported for apps whose gated tools are flat (`requireApproval`) rather than
2109
+ * one dispatcher: run it over each tool-level rule with the tool names the
2110
+ * specs cover and the ones gated conditionally.
2111
+ */
2112
+ declare function attendedFindings(name: string, rule: ConfirmRule, coverage: ApprovalCoverage | null): string[];
2057
2113
  /**
2058
2114
  * Every way a policy can be wrong without erroring, as a list of findings.
2059
2115
  * An app's test is `expect(checkConfirmationPolicy(...)).toEqual([])`.
@@ -2233,6 +2289,8 @@ interface DerivePolicyOptions {
2233
2289
  exempt?: readonly string[];
2234
2290
  /** Explicit rules that win over the verb table (e.g. a hand-written describe). */
2235
2291
  overrides?: Readonly<Record<string, ConfirmRule>>;
2292
+ /** Which actions an approval card covers; see `ApprovalCoverage`. */
2293
+ approval?: ApprovalCoverage;
2236
2294
  }
2237
2295
  interface DerivedPolicy {
2238
2296
  policy: ConfirmationPolicy & {
@@ -2763,14 +2821,20 @@ interface SprigrDataPartialUpdateOpts extends SprigrDataIndexOpts {
2763
2821
  */
2764
2822
  createIfNotExists?: boolean;
2765
2823
  /**
2766
- * Re-stamp `acl_principals` on rows already in the app's `-acl-files`
2767
- * index without rewriting the rest of each row. Every object must be
2768
- * exactly `{ objectID, acl_principals }` with a non-empty, valid principal
2769
- * list; `createIfNotExists` and `index` are refused alongside it. Use it
2770
- * when the source changes who may see an item without reporting the item
2771
- * as changed (OneDrive's delta omits sharing-only changes). Needs a
2772
- * platform with sprigr/sprigr-team#9843; older wrappers drop the flag,
2773
- * which `partialUpdateData` detects and throws on.
2824
+ * Patch rows already in the app's `-acl-files` index without rewriting
2825
+ * the rest of each row. Each object carries `objectID` plus
2826
+ * `acl_principals` (a non-empty, valid list: re-stamps who may see it)
2827
+ * and/or label fields the manifest declares in
2828
+ * `data_index.acl_patchable_fields` (for example `tags`, `properties`).
2829
+ * An object that omits `acl_principals` keeps its stored ones. `content`
2830
+ * is never patchable, and `createIfNotExists` and `index` are refused. Use
2831
+ * it when the source changes who may see an item, or only its labels,
2832
+ * without changing its content (OneDrive's delta omits sharing-only
2833
+ * changes; a Dropbox tag edit leaves the file's text as it was). The
2834
+ * re-stamp needs a platform with sprigr/sprigr-team#9843, label fields one
2835
+ * with #10371 (an older one answers `acl_partial_update_fields`: fall back
2836
+ * to a whole-row import). Older wrappers drop the flag, which
2837
+ * `partialUpdateData` detects and throws on.
2774
2838
  */
2775
2839
  withAcl?: boolean;
2776
2840
  }
@@ -3143,4 +3207,4 @@ interface ExecutionContextLike {
3143
3207
  }
3144
3208
  type HandlerFn<TArgs, TResult> = (args: TArgs, env: Record<string, unknown>, ctx?: ExecutionContextLike) => Promise<TResult>;
3145
3209
 
3146
- export { ACTOR_RETIRED_HOOK_TOOL, ACTOR_RETIRE_REASONS, APPROVAL_GRANTED_KEY, APPROVAL_PREVIEW_MAX, APP_FILES_ROOT, APP_OWNER_SEGMENT, type Actor, type ActorRetireReason, type ActorRetired, type ActorRetiredHookBody, type ActorRetiredResult, type AppApprovalEnvelope, type AppFileListItem, type AppFileUrlArgs, type AppFileUrlResult, type AppFilesEnv, type AppToolEffectsDeclaration, type AppUndoEnvelope, type ApplyPoliciesOptions, type ApprovalGateOptions, type ApprovalSpec, type ArchiveOfferOptions, type ArchiveOfferRefusal, type CallerKeyResolution, type CaptureJournal, type ConfirmCondition, type ConfirmRule, type ConfirmationPolicy, type ConnectCompleteRequest, type ConnectCompleteResult, type D1Like, type D1PreparedStatementLike, DATA_PARTIAL_UPDATE_PATH, DEFAULT_EMIT_TIMEOUT_MS, DEFAULT_LOG_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, INBOX_SEND_CONTRACT_V1, type InboxAddress, type InboxAppendRequest, type InboxAppendResult, type InboxChannelDeclaration, type InboxFoldersRequest, type InboxFoldersResult, type InboxOwner, type InboxSendConnection, type InboxSendHandlerImpl, InboxSendRefusal, type InboxSendSupports, type InboxSendV1Args, type InboxSendV1Result, type InstallBridge, InvalidUndoEnvelopeError, type JournalRowLike, type LogToPlatformOptions, type ManifestDispatchBlock, type ManifestLike, type ManifestToolLike, type MarketplaceEventEmitOpts, type MarketplaceEventSourceIntegration, NotConnectedError, type OAuthBouncerSignals, type OAuthState, type OfferUndoArgs, type PartialUpdateDataOptions, type PlatformHostEnv, type PolicyCheckInput, type PolicySource, type PutAppFileArgs, type PutAppFileResult, type PutAppFileStreamArgs, type RequireApprovalOptions, type ResolveViewerOptions, type RestoreResult, type RestoreSpec, SPRIGR_DATA_MAX_OBJECTS_PER_CALL, SPRIGR_LOG_LEVELS, SPRIGR_LOG_LIMITS, type ScheduleArgs, type SprigrDataAggregationCell, type SprigrDataAggregationResult, type SprigrDataAggregationSpec, type SprigrDataApi, type SprigrDataImportOpts, type SprigrDataIndexOpts, type SprigrDataPartialUpdateBody, type SprigrDataPartialUpdateOpts, type SprigrDataPartialUpdateResult, type SprigrDataPatch, type SprigrDataSearchOpts, type SprigrDataSearchResult, SprigrDataValidationError, type SprigrFilesApi, type SprigrFilesApiWithEdit, type SprigrFilesCreateInput, type SprigrFilesCreateResult, type SprigrFilesEditInput, type SprigrFilesEditResult, type SprigrFilesJobResult, type SprigrFilesListItem, type SprigrFilesListResult, type SprigrInboxApi, type SprigrLogEntry, type SprigrLogFn, type SprigrLogLevel, type SprigrLogResult, SprigrLogValidationError, type SyncedAttachment, type SyncedMessage, type ToolArgs, type ToolHandler, type ToolResult, type ToolWrapperOptions, UNIT_SEP, type UndoApplyOptions, type UndoApplyResult, type UndoCaptureSpec, type UndoFidelity, VIEWER_ANONYMOUS, VIEWER_CONTEXT_HEADER, VIEWER_PLATFORM_USER_ID_HEADER, VIEWER_ROLE_HEADER, VIEWER_USER_ID_HEADER, type VerbRule, type ViewerContext, type ViewerContextFailure, type ViewerContextVerification, type ViewerEnv, type WebhookArgs, type WfpBridgeEnv, type WfpEmitReply, actorKey, actorRetiredHook, actorTool, agentFileKeyFromUrl, appFileUrl, appKeyFromCallerKey, applyConfirmationPolicies, approvalHash, approvalPreview, approvalRecipients, archiveOfferRefusal, base64ToBytes, buildConfirmationPolicy, buildMarketplaceWebhookUrl, buildPartialUpdateBody, bytesToBase64, callerKeyRefusal, canEmit, canLog, canPartialUpdate, checkConfirmationPolicy, commonDestructiveVerbs, connectComplete, connectDisconnect, constantTimeEqual, createMarketplaceEmitter, createToolWrappers, decodeState, defineInboxSendHandler, deleteAppFile, deriveConfirmationPolicy, describeMissingBridge, dispatcherApproval, emitMarketplaceEvent, encodeState, fetchFileAsBase64, fetchFileBytes, fetchWithRetry, fullWarning, getAppFile, hmacSha256Hex, installTokenPost, isNotConnectedError, isStagingHost, listAppFiles, logToPlatform, manifestIsFresh, offerUndo, overlaySprigr, ownerFromActorKey, parseActor, parseActorRetiredArgs, partialUpdateData, putAppFile, putAppFileStream, randomHex, readableStoredKey, recreatedWarning, refuseWithoutForce, requireApproval, resolveInstallBridge, resolveOAuthBouncerBase, resolveOAuthBouncerCallbackUrl, resolvePlatformWebhookBase, resolveViewerContext, resolveViewerUserId, runUndoApply, safeCapture, seq, serializeManifest, set, tool, undoEnvelope, validateLogEntries, validateLogEntry, verifyViewerToken, withSprigrEmitFallback, withSprigrInboxFallback, withSprigrLogFallback };
3210
+ export { ACTOR_RETIRED_HOOK_TOOL, ACTOR_RETIRE_REASONS, APPROVAL_GRANTED_KEY, APPROVAL_PREVIEW_MAX, APP_FILES_ROOT, APP_OWNER_SEGMENT, type Actor, type ActorRetireReason, type ActorRetired, type ActorRetiredHookBody, type ActorRetiredResult, type AppApprovalEnvelope, type AppFileListItem, type AppFileUrlArgs, type AppFileUrlResult, type AppFilesEnv, type AppToolEffectsDeclaration, type AppUndoEnvelope, type ApplyPoliciesOptions, type ApprovalCoverage, type ApprovalGateOptions, type ApprovalSpec, type ArchiveOfferOptions, type ArchiveOfferRefusal, type CallerKeyResolution, type CaptureJournal, type ConfirmCondition, type ConfirmRule, type ConfirmationPolicy, type ConnectCompleteRequest, type ConnectCompleteResult, type D1Like, type D1PreparedStatementLike, DATA_PARTIAL_UPDATE_PATH, DEFAULT_EMIT_TIMEOUT_MS, DEFAULT_LOG_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, INBOX_SEND_CONTRACT_V1, type InboxAddress, type InboxAppendRequest, type InboxAppendResult, type InboxChannelDeclaration, type InboxFoldersRequest, type InboxFoldersResult, type InboxOwner, type InboxSendConnection, type InboxSendHandlerImpl, InboxSendRefusal, type InboxSendSupports, type InboxSendV1Args, type InboxSendV1Result, type InstallBridge, InvalidUndoEnvelopeError, type JournalRowLike, type LogToPlatformOptions, type ManifestDispatchBlock, type ManifestLike, type ManifestToolLike, type MarketplaceEventEmitOpts, type MarketplaceEventSourceIntegration, NotConnectedError, type OAuthBouncerSignals, type OAuthState, type OfferUndoArgs, type PartialUpdateDataOptions, type PlatformHostEnv, type PolicyCheckInput, type PolicySource, type PutAppFileArgs, type PutAppFileResult, type PutAppFileStreamArgs, type RequireApprovalOptions, type ResolveViewerOptions, type RestoreResult, type RestoreSpec, SPRIGR_DATA_MAX_OBJECTS_PER_CALL, SPRIGR_LOG_LEVELS, SPRIGR_LOG_LIMITS, type ScheduleArgs, type SprigrDataAggregationCell, type SprigrDataAggregationResult, type SprigrDataAggregationSpec, type SprigrDataApi, type SprigrDataImportOpts, type SprigrDataIndexOpts, type SprigrDataPartialUpdateBody, type SprigrDataPartialUpdateOpts, type SprigrDataPartialUpdateResult, type SprigrDataPatch, type SprigrDataSearchOpts, type SprigrDataSearchResult, SprigrDataValidationError, type SprigrFilesApi, type SprigrFilesApiWithEdit, type SprigrFilesCreateInput, type SprigrFilesCreateResult, type SprigrFilesEditInput, type SprigrFilesEditResult, type SprigrFilesJobResult, type SprigrFilesListItem, type SprigrFilesListResult, type SprigrInboxApi, type SprigrLogEntry, type SprigrLogFn, type SprigrLogLevel, type SprigrLogResult, SprigrLogValidationError, type SyncedAttachment, type SyncedMessage, type ToolArgs, type ToolHandler, type ToolResult, type ToolWrapperOptions, UNIT_SEP, type UndoApplyOptions, type UndoApplyResult, type UndoCaptureSpec, type UndoFidelity, VIEWER_ANONYMOUS, VIEWER_CONTEXT_HEADER, VIEWER_PLATFORM_USER_ID_HEADER, VIEWER_ROLE_HEADER, VIEWER_USER_ID_HEADER, type VerbRule, type ViewerContext, type ViewerContextFailure, type ViewerContextVerification, type ViewerEnv, type WebhookArgs, type WfpBridgeEnv, type WfpEmitReply, actorKey, actorRetiredHook, actorTool, agentFileKeyFromUrl, appFileUrl, appKeyFromCallerKey, applyConfirmationPolicies, approvalHash, approvalPreview, approvalRecipients, archiveOfferRefusal, attendedFindings, base64ToBytes, buildConfirmationPolicy, buildMarketplaceWebhookUrl, buildPartialUpdateBody, bytesToBase64, callerKeyRefusal, canEmit, canLog, canPartialUpdate, checkConfirmationPolicy, commonDestructiveVerbs, connectComplete, connectDisconnect, constantTimeEqual, createMarketplaceEmitter, createToolWrappers, decodeState, defineInboxSendHandler, deleteAppFile, deriveConfirmationPolicy, describeMissingBridge, dispatcherApproval, emitMarketplaceEvent, encodeState, fetchFileAsBase64, fetchFileBytes, fetchWithRetry, fullWarning, getAppFile, hmacSha256Hex, installTokenPost, isNotConnectedError, isStagingHost, listAppFiles, logToPlatform, manifestIsFresh, offerUndo, overlaySprigr, ownerFromActorKey, parseActor, parseActorRetiredArgs, partialUpdateData, putAppFile, putAppFileStream, randomHex, readableStoredKey, recreatedWarning, refuseWithoutForce, requireApproval, resolveInstallBridge, resolveOAuthBouncerBase, resolveOAuthBouncerCallbackUrl, resolvePlatformWebhookBase, resolveViewerContext, resolveViewerUserId, runUndoApply, safeCapture, seq, serializeManifest, set, tool, undoEnvelope, validateLogEntries, validateLogEntry, verifyViewerToken, withSprigrEmitFallback, withSprigrInboxFallback, withSprigrLogFallback };
package/dist/index.js CHANGED
@@ -580,19 +580,26 @@ function checkAclRestamp(objects, opts, at) {
580
580
  }
581
581
  for (let i = 0; i < objects.length; i++) {
582
582
  const o = objects[i];
583
- const extra = Object.keys(o).find((k) => k !== "objectID" && k !== "acl_principals");
584
- if (extra !== void 0) {
583
+ if ("content" in o) {
585
584
  throw new SprigrDataValidationError(
586
- `${at}: objects[${i}] carries "${extra}"; a withAcl partial update sets only objectID and acl_principals (write content with data.import withAcl)`,
585
+ `${at}: objects[${i}] carries "content"; a withAcl partial update never writes extracted text (write content with data.import withAcl)`,
587
586
  { error: "acl_partial_update_fields", index: i }
588
587
  );
589
588
  }
590
- const principals = o.acl_principals;
591
- if (!Array.isArray(principals) || principals.length === 0) {
592
- throw new SprigrDataValidationError(`${at}: objects[${i}] needs a non-empty acl_principals: string[]`, {
593
- error: "missing_acl_principals",
594
- index: i
595
- });
589
+ if (!Object.keys(o).some((k) => k !== "objectID")) {
590
+ throw new SprigrDataValidationError(
591
+ `${at}: objects[${i}] sets nothing; send acl_principals or a field your manifest declares in data_index.acl_patchable_fields`,
592
+ { error: "acl_partial_update_empty", index: i }
593
+ );
594
+ }
595
+ if ("acl_principals" in o) {
596
+ const principals = o.acl_principals;
597
+ if (!Array.isArray(principals) || principals.length === 0) {
598
+ throw new SprigrDataValidationError(`${at}: objects[${i}] needs a non-empty acl_principals: string[]`, {
599
+ error: "missing_acl_principals",
600
+ index: i
601
+ });
602
+ }
596
603
  }
597
604
  }
598
605
  }
@@ -1360,6 +1367,13 @@ async function runUndoApply(args, opts) {
1360
1367
  }
1361
1368
 
1362
1369
  // src/write-protection/confirmation-policy.ts
1370
+ function coverageSets(c) {
1371
+ if (!c) return null;
1372
+ if (c.covered == null || c.conditional == null) {
1373
+ throw new Error("approval coverage needs both `covered` and `conditional` (pass [] when no action is gated conditionally)");
1374
+ }
1375
+ return { covered: new Set(c.covered), conditional: new Set(c.conditional) };
1376
+ }
1363
1377
  function buildConfirmationPolicy(src) {
1364
1378
  const actions = {};
1365
1379
  const put = (name, rule, group) => {
@@ -1369,6 +1383,10 @@ function buildConfirmationPolicy(src) {
1369
1383
  for (const [name, describe] of Object.entries(src.irreversible ?? {})) put(name, { always: true, describe, irreversible: true }, "irreversible");
1370
1384
  for (const [name, describe] of Object.entries(src.always ?? {})) put(name, { always: true, describe }, "always");
1371
1385
  for (const [name, rule] of Object.entries(src.rules ?? {})) put(name, rule, "rules");
1386
+ const cov = coverageSets(src.approval);
1387
+ for (const name of cov?.covered ?? []) {
1388
+ if (actions[name] && !cov?.conditional.has(name)) actions[name] = { ...actions[name], attended: "approval_card" };
1389
+ }
1372
1390
  return { actions: Object.fromEntries(Object.entries(actions).sort(([a], [b]) => a.localeCompare(b))) };
1373
1391
  }
1374
1392
  var DEFAULT_WRITE_PREFIXES = [
@@ -1472,6 +1490,22 @@ var PLACEHOLDER = /\{([a-zA-Z0-9_.]+)\}/g;
1472
1490
  function conditions(rule) {
1473
1491
  return !rule.when ? [] : Array.isArray(rule.when) ? rule.when : [rule.when];
1474
1492
  }
1493
+ function attendedFindings(name, rule, coverage) {
1494
+ const cov = coverageSets(coverage ?? void 0);
1495
+ const out = [];
1496
+ const attended = rule.attended;
1497
+ const gated = rule.always === true || conditions(rule).length > 0;
1498
+ if (attended !== void 0 && attended !== "approval_card") {
1499
+ out.push(`${name}: attended must be 'approval_card' (got ${JSON.stringify(attended)}); the platform ignores anything else`);
1500
+ } else if (attended === "approval_card") {
1501
+ if (!cov) out.push(`${name}: attended: 'approval_card' cannot be checked; pass approval: { covered, conditional }`);
1502
+ else if (!cov.covered.has(name)) out.push(`${name}: attended: 'approval_card' but no approval spec covers it, so it removes the only prompt`);
1503
+ else if (cov.conditional.has(name)) out.push(`${name}: attended: 'approval_card' on a conditionally gated action, so a call that skips the card gets no prompt at all`);
1504
+ } else if (cov?.covered.has(name) && !cov.conditional.has(name) && gated) {
1505
+ out.push(`${name}: an approval spec covers it but the rule lacks attended: 'approval_card', so it asks twice`);
1506
+ }
1507
+ return out;
1508
+ }
1475
1509
  function checkConfirmationPolicy(input) {
1476
1510
  const registry = new Set(input.registry);
1477
1511
  const ungated = input.ungated ?? [];
@@ -1479,8 +1513,13 @@ function checkConfirmationPolicy(input) {
1479
1513
  const nested = input.nestedUnderInput ?? true;
1480
1514
  const money = new Set(input.moneyFields ?? DEFAULT_MONEY_FIELDS);
1481
1515
  const actions = input.policy.actions ?? {};
1516
+ const cov = coverageSets(input.approval);
1482
1517
  const out = [];
1518
+ for (const name of cov?.conditional ?? []) {
1519
+ if (!cov?.covered.has(name)) out.push(`approval.conditional: "${name}" has no approval spec (not in covered)`);
1520
+ }
1483
1521
  for (const [name, rule] of Object.entries(actions)) {
1522
+ out.push(...attendedFindings(name, rule, input.approval ?? null));
1484
1523
  if (!registry.has(name)) out.push(`dead rule: "${name}" is not in the registry, so it gates nothing`);
1485
1524
  if (!rule.always && conditions(rule).length === 0) out.push(`${name}: rule says nothing (no always, no when)`);
1486
1525
  if (!rule.describe || !rule.describe.trim()) out.push(`${name}: describe missing`);
@@ -1589,7 +1628,7 @@ function deriveConfirmationPolicy(registry, opts) {
1589
1628
  }
1590
1629
  if (!hit) unmatched.push(name);
1591
1630
  }
1592
- const policy = buildConfirmationPolicy({ irreversible, always, rules: overrides });
1631
+ const policy = buildConfirmationPolicy({ irreversible, always, rules: overrides, approval: opts.approval });
1593
1632
  return { policy, ungated, unmatched };
1594
1633
  }
1595
1634
  function commonDestructiveVerbs(subject) {
@@ -1826,6 +1865,7 @@ export {
1826
1865
  approvalPreview,
1827
1866
  approvalRecipients,
1828
1867
  archiveOfferRefusal,
1868
+ attendedFindings,
1829
1869
  base64ToBytes,
1830
1870
  buildConfirmationPolicy,
1831
1871
  buildMarketplaceWebhookUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprigr/apps-app-sdk",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",