@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 +3 -2
- package/bin/check-write-protection.mjs +28 -0
- package/dist/index.d.ts +73 -9
- package/dist/index.js +50 -10
- package/package.json +1 -1
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
|
-
*
|
|
2767
|
-
*
|
|
2768
|
-
*
|
|
2769
|
-
*
|
|
2770
|
-
*
|
|
2771
|
-
*
|
|
2772
|
-
*
|
|
2773
|
-
*
|
|
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
|
-
|
|
584
|
-
if (extra !== void 0) {
|
|
583
|
+
if ("content" in o) {
|
|
585
584
|
throw new SprigrDataValidationError(
|
|
586
|
-
`${at}: objects[${i}] carries "
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
error: "
|
|
594
|
-
|
|
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,
|