@sprigr/apps-app-sdk 0.19.0 → 0.21.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 -1
- package/bin/check-write-protection.mjs +28 -0
- package/dist/index.d.ts +309 -119
- package/dist/index.js +451 -195
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,6 +79,7 @@ export default {
|
|
|
79
79
|
- `putAppFile` / `putAppFileStream` / `appFileUrl` / `getAppFile` / `listAppFiles` / `deleteAppFile` — durable app-scoped file storage from outside the injected bridge
|
|
80
80
|
- `fetchFileBytes` / `fetchFileAsBase64` / `bytesToBase64` / `base64ToBytes` — file byte helpers
|
|
81
81
|
- `readableStoredKey(appKey, downloadUrl)` / `agentFileKeyFromUrl` — turn the install-relative key your app stored a file under into the absolute key the platform's `read_file` tool opens (read off the signed URL `appFileUrl` minted). Return it as a tool's `file_key` so an agent can open the file. `appKeyFromCallerKey` / `callerKeyRefusal` go the other way, for a key an agent hands back to your tool (0.17.0)
|
|
82
|
+
- `readFileRef(env, { file_key } | { file_url }, { maxBytes })` / `resolveFileRef`: accept a file an agent hands your tool. `resolveFileRef` only validates (run it before an approval card): a key in this install's store in any form, or an https link on `files.sprigr.com` (a link into this install is read as its key; a company link must be signed and in this company; workspace keys, other installs and every other host are refused with a `FileRefError` whose message says what to pass instead). `readFileRef` then reads the bytes with a size cap, a 20 s time cap and no redirects; `openFileRef` does the same as a stream, for a large attachment passed on without buffering. `FILE_REF_SOURCES_HELP` is the agent-facing list of accepted sources, for a tool's input description (0.21.0)
|
|
82
83
|
|
|
83
84
|
Full platform semantics: [`docs/platform-reference.md`](../../docs/platform-reference.md).
|
|
84
85
|
|
|
@@ -99,8 +100,9 @@ Three platform tiers, and they are not substitutes for each other. Full guide: [
|
|
|
99
100
|
- `undoEnvelope({...})` throws on a blank field. The platform drops the whole envelope on a blank and mints nothing, silently.
|
|
100
101
|
- `refuseWithoutForce(name, handler, opts)` / `archiveOfferRefusal(...)`: a delete refuses once and offers the vendor's native archive unless `force: true`.
|
|
101
102
|
- `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.
|
|
103
|
+
- **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
104
|
- `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
105
|
- `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`.
|
|
106
|
+
- `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
107
|
|
|
106
108
|
**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
|
@@ -164,6 +164,256 @@ declare function fetchFileAsBase64(url: string, opts?: {
|
|
|
164
164
|
size: number;
|
|
165
165
|
}>;
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* App-scoped R2 file storage (platform feature, 2026-06).
|
|
169
|
+
*
|
|
170
|
+
* Marketplace apps get a contained file store: every key is confined
|
|
171
|
+
* server-side to `_apps/{installId}/...` so an app can only ever touch
|
|
172
|
+
* its own files. The app never holds a raw R2 binding — these helpers
|
|
173
|
+
* call the platform over the same per-install token channel as
|
|
174
|
+
* `env.SPRIGR.emit` (provisioning `/internal/wfp/file/*`).
|
|
175
|
+
*
|
|
176
|
+
* Auth: `Authorization: Bearer <SPRIGR_INSTALL_TOKEN>` against
|
|
177
|
+
* `<SPRIGR_PLATFORM_BASE>/internal/wfp/file/<op>`. Both bindings are
|
|
178
|
+
* stamped onto every per-install WFP upload.
|
|
179
|
+
*
|
|
180
|
+
* Keys are app-relative (`simpro-files/123.jpg`); the platform adds and
|
|
181
|
+
* strips the `_apps/{installId}/` prefix, so the app stays unaware of it
|
|
182
|
+
* and physically cannot escape its namespace.
|
|
183
|
+
*/
|
|
184
|
+
interface AppFilesEnv {
|
|
185
|
+
SPRIGR_INSTALL_TOKEN?: string;
|
|
186
|
+
SPRIGR_PLATFORM_BASE?: string;
|
|
187
|
+
}
|
|
188
|
+
interface PutAppFileArgs {
|
|
189
|
+
/** App-relative key, e.g. "simpro-files/job-1234/55.jpg". */
|
|
190
|
+
key: string;
|
|
191
|
+
/** File bytes, base64-encoded. */
|
|
192
|
+
base64: string;
|
|
193
|
+
/** MIME type stored as the object's content-type. */
|
|
194
|
+
contentType?: string;
|
|
195
|
+
/** Optional display filename. */
|
|
196
|
+
filename?: string;
|
|
197
|
+
}
|
|
198
|
+
interface PutAppFileResult {
|
|
199
|
+
ok: true;
|
|
200
|
+
key: string;
|
|
201
|
+
bytes: number;
|
|
202
|
+
contentType: string;
|
|
203
|
+
}
|
|
204
|
+
interface PutAppFileStreamArgs {
|
|
205
|
+
/** App-relative key, e.g. "simpro-files/job-1234/55.jpg". */
|
|
206
|
+
key: string;
|
|
207
|
+
/**
|
|
208
|
+
* Raw file bytes. A `ReadableStream` is piped straight through to R2 so
|
|
209
|
+
* the bytes never fully materialise in the app isolate — use it for large
|
|
210
|
+
* blobs (full-res photos, PDFs). An `ArrayBuffer`/`Uint8Array` is also
|
|
211
|
+
* accepted when the bytes are already in hand.
|
|
212
|
+
*/
|
|
213
|
+
body: ReadableStream<Uint8Array> | ArrayBuffer | Uint8Array;
|
|
214
|
+
/** MIME type stored as the object's content-type. */
|
|
215
|
+
contentType?: string;
|
|
216
|
+
/** Optional display filename. */
|
|
217
|
+
filename?: string;
|
|
218
|
+
/**
|
|
219
|
+
* Declared byte length. Forward it when known (e.g. the source's
|
|
220
|
+
* Content-Length) so the platform can enforce its size cap before opening
|
|
221
|
+
* the R2 write. Streamed bodies carry no Content-Length, so this is the
|
|
222
|
+
* only length signal the platform gets. Omit for unknown-length streams.
|
|
223
|
+
*/
|
|
224
|
+
contentLength?: number;
|
|
225
|
+
}
|
|
226
|
+
interface AppFileUrlArgs {
|
|
227
|
+
key: string;
|
|
228
|
+
/** Seconds the signed URL stays valid (default 24h, min 60s, max ~10y, clamped server-side). */
|
|
229
|
+
expiresIn?: number;
|
|
230
|
+
}
|
|
231
|
+
interface AppFileUrlResult {
|
|
232
|
+
ok: true;
|
|
233
|
+
url: string;
|
|
234
|
+
expires_at: number;
|
|
235
|
+
key: string;
|
|
236
|
+
}
|
|
237
|
+
interface GetAppFileResult {
|
|
238
|
+
ok: true;
|
|
239
|
+
key: string;
|
|
240
|
+
base64: string;
|
|
241
|
+
contentType: string;
|
|
242
|
+
filename?: string;
|
|
243
|
+
bytes: number;
|
|
244
|
+
}
|
|
245
|
+
interface AppFileListItem {
|
|
246
|
+
key: string;
|
|
247
|
+
bytes: number;
|
|
248
|
+
uploaded?: string;
|
|
249
|
+
filename?: string;
|
|
250
|
+
contentType?: string;
|
|
251
|
+
}
|
|
252
|
+
/** Store bytes in the app's contained R2 namespace (base64 JSON). */
|
|
253
|
+
declare function putAppFile(env: AppFilesEnv, args: PutAppFileArgs): Promise<PutAppFileResult>;
|
|
254
|
+
/**
|
|
255
|
+
* Store bytes by streaming the raw body straight into the app's contained
|
|
256
|
+
* R2 namespace. Unlike `putAppFile`, this never base64-encodes the bytes
|
|
257
|
+
* and (with a `ReadableStream` body) never buffers them whole in the
|
|
258
|
+
* isolate — pipe a source `Response.body` directly through for multi-MB
|
|
259
|
+
* blobs. Metadata travels in headers; the body is opaque bytes.
|
|
260
|
+
*
|
|
261
|
+
* The platform's put-stream route cannot retry a transient R2 fault itself
|
|
262
|
+
* (its body is spent once read), so a resendable body — `ArrayBuffer` /
|
|
263
|
+
* `Uint8Array` already in hand, not a `ReadableStream` — gets a bounded
|
|
264
|
+
* retry here instead. The key is deterministic, so a retry is a full
|
|
265
|
+
* overwrite, exactly the argument the platform's buffered `put` route makes
|
|
266
|
+
* for its own retry (wfp-files.ts, sprigr-team).
|
|
267
|
+
*/
|
|
268
|
+
declare function putAppFileStream(env: AppFilesEnv, args: PutAppFileStreamArgs): Promise<PutAppFileResult>;
|
|
269
|
+
/** Mint a signed, time-limited download URL for an app file. */
|
|
270
|
+
declare function appFileUrl(env: AppFilesEnv, args: AppFileUrlArgs): Promise<AppFileUrlResult>;
|
|
271
|
+
/** Read an app file back as base64. */
|
|
272
|
+
declare function getAppFile(env: AppFilesEnv, key: string): Promise<GetAppFileResult>;
|
|
273
|
+
/** List app files under an optional app-relative prefix. */
|
|
274
|
+
declare function listAppFiles(env: AppFilesEnv, prefix?: string): Promise<{
|
|
275
|
+
ok: true;
|
|
276
|
+
files: AppFileListItem[];
|
|
277
|
+
truncated: boolean;
|
|
278
|
+
}>;
|
|
279
|
+
/** Delete an app file. */
|
|
280
|
+
declare function deleteAppFile(env: AppFilesEnv, key: string): Promise<{
|
|
281
|
+
ok: true;
|
|
282
|
+
key: string;
|
|
283
|
+
}>;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* A file an agent hands an app tool: `{ file_key }` or `{ file_url }`
|
|
287
|
+
* (sprigr/sprigr-app-kit#120).
|
|
288
|
+
*
|
|
289
|
+
* The platform resolves no file argument for an app. An agent passes a plain
|
|
290
|
+
* string and the app turns it into bytes. Three apps had each written that
|
|
291
|
+
* (trello `readInstallFile`, dropbox `resolveSource`, skusavvy `readRef`), and
|
|
292
|
+
* the copies had drifted: only dropbox checked that a company link was signed
|
|
293
|
+
* and in this company, only skusavvy capped time and refused redirects. This
|
|
294
|
+
* module is the one version, with the strictest rules of each.
|
|
295
|
+
*
|
|
296
|
+
* Two steps, usable apart:
|
|
297
|
+
*
|
|
298
|
+
* resolveFileRef validation only, no I/O. Run it before an approval card or
|
|
299
|
+
* any other side effect, so a malformed call fails first.
|
|
300
|
+
* openFileRef resolve, then open the bytes as a stream with a size cap,
|
|
301
|
+
* a time cap and no redirects, so a large attachment is
|
|
302
|
+
* passed on without being buffered first.
|
|
303
|
+
* readFileRef openFileRef, read into memory, for files an app buffers.
|
|
304
|
+
*
|
|
305
|
+
* What a reference may be:
|
|
306
|
+
* - file_key: a key in THIS install's file store, in any form a tool hands
|
|
307
|
+
* out (app-relative, absolute `_apps/<installId>/...`, or its signed link),
|
|
308
|
+
* via appKeyFromCallerKey. A key in the company's own storage (an agent's
|
|
309
|
+
* workspace file) cannot be opened by an app: refused, with how to pass it.
|
|
310
|
+
* - file_url: an https link on a Sprigr file host only. A link into this
|
|
311
|
+
* install's storage is read as a key (so it opens the caller's own copy);
|
|
312
|
+
* a link into the company's storage must be signed and in THIS company.
|
|
313
|
+
* Nothing else is fetched, so an agent cannot point an app at an arbitrary
|
|
314
|
+
* host, and every host it can reach is the platform's own.
|
|
315
|
+
*/
|
|
316
|
+
|
|
317
|
+
/** Hosts the platform serves signed file links from (the file proxy, prod and staging). */
|
|
318
|
+
declare const SPRIGR_FILE_HOSTS: ReadonlySet<string>;
|
|
319
|
+
/** Default time to read one file before giving up. */
|
|
320
|
+
declare const DEFAULT_FILE_READ_TIMEOUT_MS = 20000;
|
|
321
|
+
/** How an agent gets a workspace file to an app: a short-lived signed link. */
|
|
322
|
+
declare const WORKSPACE_FILE_HOW_TO = "Apps cannot open a file in your own Sprigr storage by key. Mint a short-lived download link for it (the files tool's generate_url; a few minutes is enough) and pass that link as file_url: the app fetches it itself, so the link never leaves Sprigr.";
|
|
323
|
+
/**
|
|
324
|
+
* The sources a file argument accepts, written for the agent. Put it in a
|
|
325
|
+
* tool's input description so the agent knows what to pass before it calls;
|
|
326
|
+
* the not_one and not_sprigr_host refusals repeat it.
|
|
327
|
+
*/
|
|
328
|
+
declare const FILE_REF_SOURCES_HELP = "Pass exactly one of: file_key, a key this app's own tools returned; or file_url, an https Sprigr file link (on files.sprigr.com), either the download_url another app's tool returned or, for a file in your own Sprigr storage, a short-lived link from the files tool's generate_url.";
|
|
329
|
+
type FileRefErrorCode = 'not_one' | 'bad_url' | 'not_sprigr_host' | 'unsigned_link' | 'other_company' | 'workspace_key' | 'other_install' | 'malformed_key' | 'internal_key' | 'no_file_store' | 'not_found' | 'too_large' | 'timeout' | 'read_failed';
|
|
330
|
+
/** A refusal or read failure, with a message written for the agent that made the call. */
|
|
331
|
+
declare class FileRefError extends Error {
|
|
332
|
+
readonly code: FileRefErrorCode;
|
|
333
|
+
constructor(code: FileRefErrorCode, message: string);
|
|
334
|
+
}
|
|
335
|
+
interface FileRef {
|
|
336
|
+
file_key?: unknown;
|
|
337
|
+
file_url?: unknown;
|
|
338
|
+
}
|
|
339
|
+
interface ResolveFileRefOptions {
|
|
340
|
+
/** env.INSTALL_ID: refuses an absolute key or link for another install. */
|
|
341
|
+
installId?: string | null;
|
|
342
|
+
/** env.COMPANY_ID: a company link must be in this company; also spots workspace keys. */
|
|
343
|
+
companyId?: string | null;
|
|
344
|
+
/** App-relative prefixes of the app's own working storage, never a tool's output. */
|
|
345
|
+
internalPrefixes?: readonly string[];
|
|
346
|
+
/** Field name used in messages (default `file`), e.g. `image` or `images[1]`. */
|
|
347
|
+
label?: string;
|
|
348
|
+
}
|
|
349
|
+
type ResolvedFileRef = {
|
|
350
|
+
kind: 'stored';
|
|
351
|
+
appKey: string;
|
|
352
|
+
filename: string;
|
|
353
|
+
} | {
|
|
354
|
+
kind: 'url';
|
|
355
|
+
url: string;
|
|
356
|
+
filename: string;
|
|
357
|
+
};
|
|
358
|
+
/**
|
|
359
|
+
* Check a `{ file_key } | { file_url }` reference and say what to read.
|
|
360
|
+
* Pure: no request is made. Throws FileRefError.
|
|
361
|
+
*/
|
|
362
|
+
declare function resolveFileRef(ref: FileRef | null | undefined, opts?: ResolveFileRefOptions): ResolvedFileRef;
|
|
363
|
+
interface FileRefEnv extends Partial<AppFilesEnv> {
|
|
364
|
+
INSTALL_ID?: string;
|
|
365
|
+
COMPANY_ID?: string;
|
|
366
|
+
/** Present on dispatched handlers: env.SPRIGR.files.url mints a read link. */
|
|
367
|
+
SPRIGR?: unknown;
|
|
368
|
+
}
|
|
369
|
+
interface ReadFileRefOptions extends Omit<ResolveFileRefOptions, 'installId' | 'companyId'> {
|
|
370
|
+
/** Byte ceiling for one file (required: pick the downstream API's limit). */
|
|
371
|
+
maxBytes: number;
|
|
372
|
+
/**
|
|
373
|
+
* Time for the whole read, headers and body, default
|
|
374
|
+
* {@link DEFAULT_FILE_READ_TIMEOUT_MS}. A streaming caller sending a large
|
|
375
|
+
* file onward should raise it to cover the upload too.
|
|
376
|
+
*/
|
|
377
|
+
timeoutMs?: number;
|
|
378
|
+
}
|
|
379
|
+
/** How the bytes were read: a minted link to a stored key, the install-token fallback, or the caller's link. */
|
|
380
|
+
type FileRefVia = 'bridge' | 'install_token' | 'url';
|
|
381
|
+
interface FileRefContent {
|
|
382
|
+
bytes: Uint8Array;
|
|
383
|
+
size: number;
|
|
384
|
+
contentType: string;
|
|
385
|
+
filename: string;
|
|
386
|
+
via: FileRefVia;
|
|
387
|
+
}
|
|
388
|
+
interface FileRefStream {
|
|
389
|
+
/**
|
|
390
|
+
* The file's bytes. Errors with a FileRefError (`too_large`, `timeout`,
|
|
391
|
+
* `read_failed`) if the cap or the time runs out mid-read; cancel it if you
|
|
392
|
+
* stop early.
|
|
393
|
+
*/
|
|
394
|
+
body: ReadableStream<Uint8Array>;
|
|
395
|
+
/** The declared length when the source sent one, else null. */
|
|
396
|
+
size: number | null;
|
|
397
|
+
contentType: string;
|
|
398
|
+
filename: string;
|
|
399
|
+
via: FileRefVia;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Resolve a reference (with the env's install and company) and open it as a
|
|
403
|
+
* stream, so a large attachment is passed on without being buffered first.
|
|
404
|
+
* Every fetch is time-capped, size-capped (on the declared length up front and
|
|
405
|
+
* on the bytes as they arrive), and refuses redirects, so a Sprigr link cannot
|
|
406
|
+
* bounce the read to another host. The install-token fallback returns its
|
|
407
|
+
* bytes in one chunk (that transport is base64 JSON). Throws FileRefError.
|
|
408
|
+
*/
|
|
409
|
+
declare function openFileRef(env: FileRefEnv, ref: FileRef | null | undefined, opts: ReadFileRefOptions): Promise<FileRefStream>;
|
|
410
|
+
/**
|
|
411
|
+
* {@link openFileRef}, read into memory. For files an app sends on in one
|
|
412
|
+
* request body; use openFileRef to pass a large file on as a stream.
|
|
413
|
+
* Throws FileRefError.
|
|
414
|
+
*/
|
|
415
|
+
declare function readFileRef(env: FileRefEnv, ref: FileRef | null | undefined, opts: ReadFileRefOptions): Promise<FileRefContent>;
|
|
416
|
+
|
|
167
417
|
/**
|
|
168
418
|
* Per-env resolver for the platform webhook host that marketplace
|
|
169
419
|
* apps use as the callback URL when registering provider webhooks.
|
|
@@ -1292,124 +1542,6 @@ declare function parseActorRetiredArgs(args: unknown): ActorRetired;
|
|
|
1292
1542
|
*/
|
|
1293
1543
|
declare function actorRetiredHook<Env>(fn: (env: Env, retired: ActorRetired) => Promise<ActorRetiredResult>): (args: unknown, env: Env) => Promise<ActorRetiredResult>;
|
|
1294
1544
|
|
|
1295
|
-
/**
|
|
1296
|
-
* App-scoped R2 file storage (platform feature, 2026-06).
|
|
1297
|
-
*
|
|
1298
|
-
* Marketplace apps get a contained file store: every key is confined
|
|
1299
|
-
* server-side to `_apps/{installId}/...` so an app can only ever touch
|
|
1300
|
-
* its own files. The app never holds a raw R2 binding — these helpers
|
|
1301
|
-
* call the platform over the same per-install token channel as
|
|
1302
|
-
* `env.SPRIGR.emit` (provisioning `/internal/wfp/file/*`).
|
|
1303
|
-
*
|
|
1304
|
-
* Auth: `Authorization: Bearer <SPRIGR_INSTALL_TOKEN>` against
|
|
1305
|
-
* `<SPRIGR_PLATFORM_BASE>/internal/wfp/file/<op>`. Both bindings are
|
|
1306
|
-
* stamped onto every per-install WFP upload.
|
|
1307
|
-
*
|
|
1308
|
-
* Keys are app-relative (`simpro-files/123.jpg`); the platform adds and
|
|
1309
|
-
* strips the `_apps/{installId}/` prefix, so the app stays unaware of it
|
|
1310
|
-
* and physically cannot escape its namespace.
|
|
1311
|
-
*/
|
|
1312
|
-
interface AppFilesEnv {
|
|
1313
|
-
SPRIGR_INSTALL_TOKEN?: string;
|
|
1314
|
-
SPRIGR_PLATFORM_BASE?: string;
|
|
1315
|
-
}
|
|
1316
|
-
interface PutAppFileArgs {
|
|
1317
|
-
/** App-relative key, e.g. "simpro-files/job-1234/55.jpg". */
|
|
1318
|
-
key: string;
|
|
1319
|
-
/** File bytes, base64-encoded. */
|
|
1320
|
-
base64: string;
|
|
1321
|
-
/** MIME type stored as the object's content-type. */
|
|
1322
|
-
contentType?: string;
|
|
1323
|
-
/** Optional display filename. */
|
|
1324
|
-
filename?: string;
|
|
1325
|
-
}
|
|
1326
|
-
interface PutAppFileResult {
|
|
1327
|
-
ok: true;
|
|
1328
|
-
key: string;
|
|
1329
|
-
bytes: number;
|
|
1330
|
-
contentType: string;
|
|
1331
|
-
}
|
|
1332
|
-
interface PutAppFileStreamArgs {
|
|
1333
|
-
/** App-relative key, e.g. "simpro-files/job-1234/55.jpg". */
|
|
1334
|
-
key: string;
|
|
1335
|
-
/**
|
|
1336
|
-
* Raw file bytes. A `ReadableStream` is piped straight through to R2 so
|
|
1337
|
-
* the bytes never fully materialise in the app isolate — use it for large
|
|
1338
|
-
* blobs (full-res photos, PDFs). An `ArrayBuffer`/`Uint8Array` is also
|
|
1339
|
-
* accepted when the bytes are already in hand.
|
|
1340
|
-
*/
|
|
1341
|
-
body: ReadableStream<Uint8Array> | ArrayBuffer | Uint8Array;
|
|
1342
|
-
/** MIME type stored as the object's content-type. */
|
|
1343
|
-
contentType?: string;
|
|
1344
|
-
/** Optional display filename. */
|
|
1345
|
-
filename?: string;
|
|
1346
|
-
/**
|
|
1347
|
-
* Declared byte length. Forward it when known (e.g. the source's
|
|
1348
|
-
* Content-Length) so the platform can enforce its size cap before opening
|
|
1349
|
-
* the R2 write. Streamed bodies carry no Content-Length, so this is the
|
|
1350
|
-
* only length signal the platform gets. Omit for unknown-length streams.
|
|
1351
|
-
*/
|
|
1352
|
-
contentLength?: number;
|
|
1353
|
-
}
|
|
1354
|
-
interface AppFileUrlArgs {
|
|
1355
|
-
key: string;
|
|
1356
|
-
/** Seconds the signed URL stays valid (default 24h, min 60s, max ~10y, clamped server-side). */
|
|
1357
|
-
expiresIn?: number;
|
|
1358
|
-
}
|
|
1359
|
-
interface AppFileUrlResult {
|
|
1360
|
-
ok: true;
|
|
1361
|
-
url: string;
|
|
1362
|
-
expires_at: number;
|
|
1363
|
-
key: string;
|
|
1364
|
-
}
|
|
1365
|
-
interface GetAppFileResult {
|
|
1366
|
-
ok: true;
|
|
1367
|
-
key: string;
|
|
1368
|
-
base64: string;
|
|
1369
|
-
contentType: string;
|
|
1370
|
-
filename?: string;
|
|
1371
|
-
bytes: number;
|
|
1372
|
-
}
|
|
1373
|
-
interface AppFileListItem {
|
|
1374
|
-
key: string;
|
|
1375
|
-
bytes: number;
|
|
1376
|
-
uploaded?: string;
|
|
1377
|
-
filename?: string;
|
|
1378
|
-
contentType?: string;
|
|
1379
|
-
}
|
|
1380
|
-
/** Store bytes in the app's contained R2 namespace (base64 JSON). */
|
|
1381
|
-
declare function putAppFile(env: AppFilesEnv, args: PutAppFileArgs): Promise<PutAppFileResult>;
|
|
1382
|
-
/**
|
|
1383
|
-
* Store bytes by streaming the raw body straight into the app's contained
|
|
1384
|
-
* R2 namespace. Unlike `putAppFile`, this never base64-encodes the bytes
|
|
1385
|
-
* and (with a `ReadableStream` body) never buffers them whole in the
|
|
1386
|
-
* isolate — pipe a source `Response.body` directly through for multi-MB
|
|
1387
|
-
* blobs. Metadata travels in headers; the body is opaque bytes.
|
|
1388
|
-
*
|
|
1389
|
-
* The platform's put-stream route cannot retry a transient R2 fault itself
|
|
1390
|
-
* (its body is spent once read), so a resendable body — `ArrayBuffer` /
|
|
1391
|
-
* `Uint8Array` already in hand, not a `ReadableStream` — gets a bounded
|
|
1392
|
-
* retry here instead. The key is deterministic, so a retry is a full
|
|
1393
|
-
* overwrite, exactly the argument the platform's buffered `put` route makes
|
|
1394
|
-
* for its own retry (wfp-files.ts, sprigr-team).
|
|
1395
|
-
*/
|
|
1396
|
-
declare function putAppFileStream(env: AppFilesEnv, args: PutAppFileStreamArgs): Promise<PutAppFileResult>;
|
|
1397
|
-
/** Mint a signed, time-limited download URL for an app file. */
|
|
1398
|
-
declare function appFileUrl(env: AppFilesEnv, args: AppFileUrlArgs): Promise<AppFileUrlResult>;
|
|
1399
|
-
/** Read an app file back as base64. */
|
|
1400
|
-
declare function getAppFile(env: AppFilesEnv, key: string): Promise<GetAppFileResult>;
|
|
1401
|
-
/** List app files under an optional app-relative prefix. */
|
|
1402
|
-
declare function listAppFiles(env: AppFilesEnv, prefix?: string): Promise<{
|
|
1403
|
-
ok: true;
|
|
1404
|
-
files: AppFileListItem[];
|
|
1405
|
-
truncated: boolean;
|
|
1406
|
-
}>;
|
|
1407
|
-
/** Delete an app file. */
|
|
1408
|
-
declare function deleteAppFile(env: AppFilesEnv, key: string): Promise<{
|
|
1409
|
-
ok: true;
|
|
1410
|
-
key: string;
|
|
1411
|
-
}>;
|
|
1412
|
-
|
|
1413
1545
|
/**
|
|
1414
1546
|
* Write-protection types, mirrored from the Sprigr platform.
|
|
1415
1547
|
*
|
|
@@ -1453,6 +1585,18 @@ interface ConfirmRule {
|
|
|
1453
1585
|
describe?: string;
|
|
1454
1586
|
/** Appends "which cannot be undone" so the model cannot undersell it. */
|
|
1455
1587
|
irreversible?: boolean;
|
|
1588
|
+
/**
|
|
1589
|
+
* `'approval_card'`: this action's human-approval card (an approval spec)
|
|
1590
|
+
* covers it whenever a person is on the turn, so the platform skips the
|
|
1591
|
+
* confirmation there and the person is asked once, by the card. Unattended
|
|
1592
|
+
* runs still confirm. Never hand-write it: pass `approval: { covered,
|
|
1593
|
+
* conditional }` to `buildConfirmationPolicy` / `deriveConfirmationPolicy`
|
|
1594
|
+
* so it follows the specs, and to `checkConfirmationPolicy` so a stale or
|
|
1595
|
+
* conditionally gated key fails the test.
|
|
1596
|
+
* Needs a platform with sprigr-team decision 0167; an older one ignores it
|
|
1597
|
+
* and keeps asking twice.
|
|
1598
|
+
*/
|
|
1599
|
+
attended?: 'approval_card';
|
|
1456
1600
|
}
|
|
1457
1601
|
interface ConfirmCondition {
|
|
1458
1602
|
count: string;
|
|
@@ -2021,6 +2165,34 @@ interface PolicySource {
|
|
|
2021
2165
|
always?: Record<string, string>;
|
|
2022
2166
|
/** Full rules, for thresholds and anything the two maps above cannot express. */
|
|
2023
2167
|
rules?: Record<string, ConfirmRule>;
|
|
2168
|
+
/**
|
|
2169
|
+
* Which actions an approval card covers; see {@link ApprovalCoverage}. Each
|
|
2170
|
+
* covered action that also has a rule here, and is not `conditional`, gets
|
|
2171
|
+
* `attended: 'approval_card'`, so the person is asked once on an attended
|
|
2172
|
+
* turn (the card) instead of twice.
|
|
2173
|
+
*/
|
|
2174
|
+
approval?: ApprovalCoverage;
|
|
2175
|
+
}
|
|
2176
|
+
/**
|
|
2177
|
+
* Which actions an approval card covers, for deriving and checking
|
|
2178
|
+
* `attended: 'approval_card'` (sprigr-team decision 0167).
|
|
2179
|
+
*
|
|
2180
|
+
* Both fields are required on purpose. The SDK can see an app's approval
|
|
2181
|
+
* specs but not any condition the app wraps around the gate (simpro sends its
|
|
2182
|
+
* deletes to the gate only with `force: true`), and an action marked attended
|
|
2183
|
+
* whose call skips the card would skip the confirmation too: no prompt at all.
|
|
2184
|
+
* So every app states its conditional actions, `[]` when it has none, and the
|
|
2185
|
+
* builder and the check refuse to mark them.
|
|
2186
|
+
*/
|
|
2187
|
+
interface ApprovalCoverage {
|
|
2188
|
+
/** Every action with an approval spec: the keys of the specs object you hand to the gate. */
|
|
2189
|
+
covered: Iterable<string>;
|
|
2190
|
+
/**
|
|
2191
|
+
* Covered actions the app sends through the gate only under a condition of
|
|
2192
|
+
* its own (simpro's `GATE_ONLY_WHEN`). Never marked attended; they keep the
|
|
2193
|
+
* confirmation. Pass `[]` when every covered action always reaches the gate.
|
|
2194
|
+
*/
|
|
2195
|
+
conditional: Iterable<string>;
|
|
2024
2196
|
}
|
|
2025
2197
|
/** Build a sorted `{ actions }` policy. An action named in two groups throws. */
|
|
2026
2198
|
declare function buildConfirmationPolicy(src: PolicySource): ConfirmationPolicy;
|
|
@@ -2053,7 +2225,23 @@ interface PolicyCheckInput {
|
|
|
2053
2225
|
*/
|
|
2054
2226
|
nestedUnderInput?: boolean;
|
|
2055
2227
|
moneyFields?: readonly string[];
|
|
2228
|
+
/**
|
|
2229
|
+
* Which actions an approval card covers. With it, the check fails a rule
|
|
2230
|
+
* carrying `attended: 'approval_card'` on an action no spec covers or that
|
|
2231
|
+
* the app gates conditionally (either way the key removes the person's only
|
|
2232
|
+
* prompt), a covered, unconditional, gated action that lacks the key (it asks
|
|
2233
|
+
* twice), and a `conditional` entry with no spec. Without it, any `attended`
|
|
2234
|
+
* key is itself a finding, because nothing can vouch for it.
|
|
2235
|
+
*/
|
|
2236
|
+
approval?: ApprovalCoverage;
|
|
2056
2237
|
}
|
|
2238
|
+
/**
|
|
2239
|
+
* Findings for one rule's `attended` key against the approval coverage.
|
|
2240
|
+
* Exported for apps whose gated tools are flat (`requireApproval`) rather than
|
|
2241
|
+
* one dispatcher: run it over each tool-level rule with the tool names the
|
|
2242
|
+
* specs cover and the ones gated conditionally.
|
|
2243
|
+
*/
|
|
2244
|
+
declare function attendedFindings(name: string, rule: ConfirmRule, coverage: ApprovalCoverage | null): string[];
|
|
2057
2245
|
/**
|
|
2058
2246
|
* Every way a policy can be wrong without erroring, as a list of findings.
|
|
2059
2247
|
* An app's test is `expect(checkConfirmationPolicy(...)).toEqual([])`.
|
|
@@ -2233,6 +2421,8 @@ interface DerivePolicyOptions {
|
|
|
2233
2421
|
exempt?: readonly string[];
|
|
2234
2422
|
/** Explicit rules that win over the verb table (e.g. a hand-written describe). */
|
|
2235
2423
|
overrides?: Readonly<Record<string, ConfirmRule>>;
|
|
2424
|
+
/** Which actions an approval card covers; see `ApprovalCoverage`. */
|
|
2425
|
+
approval?: ApprovalCoverage;
|
|
2236
2426
|
}
|
|
2237
2427
|
interface DerivedPolicy {
|
|
2238
2428
|
policy: ConfirmationPolicy & {
|
|
@@ -3149,4 +3339,4 @@ interface ExecutionContextLike {
|
|
|
3149
3339
|
}
|
|
3150
3340
|
type HandlerFn<TArgs, TResult> = (args: TArgs, env: Record<string, unknown>, ctx?: ExecutionContextLike) => Promise<TResult>;
|
|
3151
3341
|
|
|
3152
|
-
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 };
|
|
3342
|
+
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_FILE_READ_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, FILE_REF_SOURCES_HELP, type FetchFileResult, type FetchWithRetryOptions, type FileRef, type FileRefContent, type FileRefEnv, FileRefError, type FileRefErrorCode, type FileRefStream, type FileRefVia, 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 ReadFileRefOptions, type RequireApprovalOptions, type ResolveFileRefOptions, type ResolveViewerOptions, type ResolvedFileRef, type RestoreResult, type RestoreSpec, SPRIGR_DATA_MAX_OBJECTS_PER_CALL, SPRIGR_FILE_HOSTS, 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, WORKSPACE_FILE_HOW_TO, 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, openFileRef, overlaySprigr, ownerFromActorKey, parseActor, parseActorRetiredArgs, partialUpdateData, putAppFile, putAppFileStream, randomHex, readFileRef, readableStoredKey, recreatedWarning, refuseWithoutForce, requireApproval, resolveFileRef, resolveInstallBridge, resolveOAuthBouncerBase, resolveOAuthBouncerCallbackUrl, resolvePlatformWebhookBase, resolveViewerContext, resolveViewerUserId, runUndoApply, safeCapture, seq, serializeManifest, set, tool, undoEnvelope, validateLogEntries, validateLogEntry, verifyViewerToken, withSprigrEmitFallback, withSprigrInboxFallback, withSprigrLogFallback };
|
package/dist/index.js
CHANGED
|
@@ -155,6 +155,409 @@ async function fetchFileAsBase64(url, opts = {}) {
|
|
|
155
155
|
return { base64: bytesToBase64(bytes), contentType, size };
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
// src/app-files.ts
|
|
159
|
+
var DEFAULT_PLATFORM_BASE = "https://webhooks.sprigr.com";
|
|
160
|
+
function platformBase(env) {
|
|
161
|
+
return (env.SPRIGR_PLATFORM_BASE ?? DEFAULT_PLATFORM_BASE).trim().replace(/\/+$/, "");
|
|
162
|
+
}
|
|
163
|
+
var AppFilesError = class extends Error {
|
|
164
|
+
status;
|
|
165
|
+
constructor(message, status) {
|
|
166
|
+
super(message);
|
|
167
|
+
this.name = "AppFilesError";
|
|
168
|
+
this.status = status;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
function requireToken(env) {
|
|
172
|
+
const token = env.SPRIGR_INSTALL_TOKEN;
|
|
173
|
+
if (!token) {
|
|
174
|
+
throw new AppFilesError("app file storage unavailable: SPRIGR_INSTALL_TOKEN unset", 500);
|
|
175
|
+
}
|
|
176
|
+
return token;
|
|
177
|
+
}
|
|
178
|
+
async function parseResult(res, op) {
|
|
179
|
+
const text = await res.text();
|
|
180
|
+
let parsed = void 0;
|
|
181
|
+
try {
|
|
182
|
+
parsed = text ? JSON.parse(text) : void 0;
|
|
183
|
+
} catch {
|
|
184
|
+
}
|
|
185
|
+
if (!res.ok) {
|
|
186
|
+
const detail = parsed && typeof parsed === "object" ? parsed.hint ?? parsed.detail ?? parsed.error : text.slice(0, 200);
|
|
187
|
+
throw new AppFilesError(`app file ${op} failed (${res.status}): ${String(detail)}`, res.status);
|
|
188
|
+
}
|
|
189
|
+
return parsed;
|
|
190
|
+
}
|
|
191
|
+
async function call(env, op, body) {
|
|
192
|
+
const token = requireToken(env);
|
|
193
|
+
const res = await fetch(`${platformBase(env)}/internal/wfp/file/${op}`, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers: {
|
|
196
|
+
authorization: `Bearer ${token}`,
|
|
197
|
+
"content-type": "application/json"
|
|
198
|
+
},
|
|
199
|
+
body: JSON.stringify(body ?? {})
|
|
200
|
+
});
|
|
201
|
+
return parseResult(res, op);
|
|
202
|
+
}
|
|
203
|
+
function putAppFile(env, args) {
|
|
204
|
+
return call(env, "put", {
|
|
205
|
+
key: args.key,
|
|
206
|
+
base64: args.base64,
|
|
207
|
+
contentType: args.contentType,
|
|
208
|
+
filename: args.filename
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
var STREAM_PUT_MAX_ATTEMPTS = 3;
|
|
212
|
+
var STREAM_PUT_BASE_DELAY_MS = 500;
|
|
213
|
+
function isTransientPutStreamFailure(status, message) {
|
|
214
|
+
if (status === 503) return true;
|
|
215
|
+
if (status !== 502) return false;
|
|
216
|
+
const msg = message.toLowerCase();
|
|
217
|
+
return msg.includes("(10001)") || msg.includes("(10043)");
|
|
218
|
+
}
|
|
219
|
+
function sleep2(ms) {
|
|
220
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
221
|
+
}
|
|
222
|
+
async function putAppFileStream(env, args) {
|
|
223
|
+
const token = requireToken(env);
|
|
224
|
+
const headers = {
|
|
225
|
+
authorization: `Bearer ${token}`,
|
|
226
|
+
"content-type": args.contentType || "application/octet-stream",
|
|
227
|
+
"x-app-file-key": args.key,
|
|
228
|
+
"x-app-file-content-type": args.contentType || "application/octet-stream"
|
|
229
|
+
};
|
|
230
|
+
if (args.filename) headers["x-app-file-name"] = encodeURIComponent(args.filename);
|
|
231
|
+
if (typeof args.contentLength === "number" && Number.isFinite(args.contentLength)) {
|
|
232
|
+
headers["x-app-file-length"] = String(args.contentLength);
|
|
233
|
+
}
|
|
234
|
+
const resendable = !(args.body instanceof ReadableStream);
|
|
235
|
+
const attempts = resendable ? STREAM_PUT_MAX_ATTEMPTS : 1;
|
|
236
|
+
let lastErr;
|
|
237
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
238
|
+
const res = await fetch(`${platformBase(env)}/internal/wfp/file/put-stream`, {
|
|
239
|
+
method: "POST",
|
|
240
|
+
headers,
|
|
241
|
+
body: args.body,
|
|
242
|
+
// Required by the Workers runtime when sending a streaming request body.
|
|
243
|
+
...args.body instanceof ReadableStream ? { duplex: "half" } : {}
|
|
244
|
+
});
|
|
245
|
+
try {
|
|
246
|
+
return await parseResult(res, "put-stream");
|
|
247
|
+
} catch (err) {
|
|
248
|
+
lastErr = err;
|
|
249
|
+
if (!resendable || attempt === attempts || !(err instanceof AppFilesError) || !isTransientPutStreamFailure(err.status, err.message)) {
|
|
250
|
+
throw err;
|
|
251
|
+
}
|
|
252
|
+
await sleep2(STREAM_PUT_BASE_DELAY_MS * 2 ** (attempt - 1));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
throw lastErr;
|
|
256
|
+
}
|
|
257
|
+
function appFileUrl(env, args) {
|
|
258
|
+
return call(env, "url", { key: args.key, expiresIn: args.expiresIn });
|
|
259
|
+
}
|
|
260
|
+
function getAppFile(env, key) {
|
|
261
|
+
return call(env, "get", { key });
|
|
262
|
+
}
|
|
263
|
+
function listAppFiles(env, prefix) {
|
|
264
|
+
return call(env, "list", { prefix });
|
|
265
|
+
}
|
|
266
|
+
function deleteAppFile(env, key) {
|
|
267
|
+
return call(env, "delete", { key });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/stored-file-keys.ts
|
|
271
|
+
var APP_FILES_ROOT = "_apps/";
|
|
272
|
+
var APP_OWNER_SEGMENT = "~u";
|
|
273
|
+
function isSegment(s) {
|
|
274
|
+
return typeof s === "string" && s.length > 0 && !s.includes("/");
|
|
275
|
+
}
|
|
276
|
+
function trimLeadingSlashes(key) {
|
|
277
|
+
return key.replace(/^\/+/, "");
|
|
278
|
+
}
|
|
279
|
+
function parseAbsolute(key) {
|
|
280
|
+
if (!key.startsWith(APP_FILES_ROOT)) return null;
|
|
281
|
+
const parts = key.slice(APP_FILES_ROOT.length).split("/");
|
|
282
|
+
const installId = parts[0];
|
|
283
|
+
if (!isSegment(installId)) return null;
|
|
284
|
+
let i = 1;
|
|
285
|
+
let ownerHash = null;
|
|
286
|
+
if (parts[1] === APP_OWNER_SEGMENT) {
|
|
287
|
+
if (!isSegment(parts[2])) return null;
|
|
288
|
+
ownerHash = parts[2];
|
|
289
|
+
i = 3;
|
|
290
|
+
}
|
|
291
|
+
const rest = parts.slice(i).join("/");
|
|
292
|
+
if (!rest || rest.split("/").some((seg) => seg === "")) return null;
|
|
293
|
+
return { installId, ownerHash, rest };
|
|
294
|
+
}
|
|
295
|
+
function urlPathKey(url) {
|
|
296
|
+
try {
|
|
297
|
+
return trimLeadingSlashes(decodeURIComponent(new URL(url).pathname));
|
|
298
|
+
} catch {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function agentFileKeyFromUrl(downloadUrl, appKey) {
|
|
303
|
+
const rel = trimLeadingSlashes(appKey);
|
|
304
|
+
if (!rel) return null;
|
|
305
|
+
const path = urlPathKey(downloadUrl);
|
|
306
|
+
if (!path) return null;
|
|
307
|
+
const parsed = parseAbsolute(path);
|
|
308
|
+
if (!parsed || parsed.rest !== rel) return null;
|
|
309
|
+
return path;
|
|
310
|
+
}
|
|
311
|
+
function readableStoredKey(appKey, downloadUrl) {
|
|
312
|
+
return agentFileKeyFromUrl(downloadUrl, appKey) ?? appKey;
|
|
313
|
+
}
|
|
314
|
+
function appKeyFromCallerKey(key, ownInstallId) {
|
|
315
|
+
let candidate = key.trim();
|
|
316
|
+
let wasAbsolute = false;
|
|
317
|
+
if (/^https?:\/\//i.test(candidate)) {
|
|
318
|
+
const path = urlPathKey(candidate);
|
|
319
|
+
if (!path || !path.startsWith(APP_FILES_ROOT)) return { ok: false, reason: "malformed" };
|
|
320
|
+
candidate = path;
|
|
321
|
+
}
|
|
322
|
+
candidate = trimLeadingSlashes(candidate);
|
|
323
|
+
if (candidate.startsWith(APP_FILES_ROOT)) {
|
|
324
|
+
const parsed = parseAbsolute(candidate);
|
|
325
|
+
if (!parsed) return { ok: false, reason: "malformed" };
|
|
326
|
+
if (ownInstallId && parsed.installId !== ownInstallId) {
|
|
327
|
+
return { ok: false, reason: "other_install", installId: parsed.installId };
|
|
328
|
+
}
|
|
329
|
+
return { ok: true, appKey: parsed.rest, wasAbsolute: true };
|
|
330
|
+
}
|
|
331
|
+
const parts = candidate.split("/");
|
|
332
|
+
if (parts[0] === APP_OWNER_SEGMENT) {
|
|
333
|
+
const rest = parts.slice(2).join("/");
|
|
334
|
+
if (!isSegment(parts[1]) || !rest) return { ok: false, reason: "malformed" };
|
|
335
|
+
return { ok: true, appKey: rest, wasAbsolute: true };
|
|
336
|
+
}
|
|
337
|
+
return { ok: true, appKey: candidate, wasAbsolute };
|
|
338
|
+
}
|
|
339
|
+
function callerKeyRefusal(key, r) {
|
|
340
|
+
if (r.reason === "other_install") {
|
|
341
|
+
return `"${key}" is a stored file of a different app install (${r.installId}), not this one. Pass a file_key this app returned to you.`;
|
|
342
|
+
}
|
|
343
|
+
return `"${key}" is not a stored file key: it has no file path after the app storage prefix.`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/file-ref.ts
|
|
347
|
+
var SPRIGR_FILE_HOSTS = /* @__PURE__ */ new Set(["files.sprigr.com", "staging-files.sprigr.com"]);
|
|
348
|
+
var DEFAULT_FILE_READ_TIMEOUT_MS = 2e4;
|
|
349
|
+
var STORED_READ_URL_TTL_SECONDS = 300;
|
|
350
|
+
var WORKSPACE_FILE_HOW_TO = "Apps cannot open a file in your own Sprigr storage by key. Mint a short-lived download link for it (the files tool's generate_url; a few minutes is enough) and pass that link as file_url: the app fetches it itself, so the link never leaves Sprigr.";
|
|
351
|
+
var FILE_REF_SOURCES_HELP = "Pass exactly one of: file_key, a key this app's own tools returned; or file_url, an https Sprigr file link (on files.sprigr.com), either the download_url another app's tool returned or, for a file in your own Sprigr storage, a short-lived link from the files tool's generate_url.";
|
|
352
|
+
var FileRefError = class extends Error {
|
|
353
|
+
code;
|
|
354
|
+
constructor(code, message) {
|
|
355
|
+
super(message);
|
|
356
|
+
this.name = "FileRefError";
|
|
357
|
+
this.code = code;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
function str(v) {
|
|
361
|
+
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
362
|
+
}
|
|
363
|
+
function lastSegment(path, fallback) {
|
|
364
|
+
const tail = path.split("/").filter(Boolean).pop();
|
|
365
|
+
if (!tail) return fallback;
|
|
366
|
+
try {
|
|
367
|
+
return decodeURIComponent(tail);
|
|
368
|
+
} catch {
|
|
369
|
+
return tail;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function isWorkspaceKey(key, companyId) {
|
|
373
|
+
const k = key.replace(/^\/+/, "");
|
|
374
|
+
if (k.startsWith(APP_FILES_ROOT)) return false;
|
|
375
|
+
if (companyId && k.startsWith(`${companyId}/`)) return true;
|
|
376
|
+
return /^(?:comp_|agt_)[^/]+\//.test(k);
|
|
377
|
+
}
|
|
378
|
+
function stored(appKey, original, opts) {
|
|
379
|
+
if ((opts.internalPrefixes ?? []).some((p) => appKey.startsWith(p))) {
|
|
380
|
+
throw new FileRefError("internal_key", `"${original}" is this app's internal working storage, not a file a tool returned. Pass a file_key a tool gave you.`);
|
|
381
|
+
}
|
|
382
|
+
return { kind: "stored", appKey, filename: lastSegment(appKey, "file") };
|
|
383
|
+
}
|
|
384
|
+
function resolveFileRef(ref, opts = {}) {
|
|
385
|
+
const label = opts.label ?? "file";
|
|
386
|
+
const key = str(ref?.file_key);
|
|
387
|
+
const url = str(ref?.file_url);
|
|
388
|
+
if (key === null === (url === null)) {
|
|
389
|
+
throw new FileRefError("not_one", `${label} takes exactly one of file_key or file_url. ${FILE_REF_SOURCES_HELP}`);
|
|
390
|
+
}
|
|
391
|
+
if (url !== null) {
|
|
392
|
+
let u;
|
|
393
|
+
let path;
|
|
394
|
+
try {
|
|
395
|
+
u = new URL(url);
|
|
396
|
+
path = decodeURIComponent(u.pathname).replace(/^\/+/, "");
|
|
397
|
+
} catch {
|
|
398
|
+
throw new FileRefError("bad_url", `${label}.file_url is not a valid URL.`);
|
|
399
|
+
}
|
|
400
|
+
if (u.protocol !== "https:" || !SPRIGR_FILE_HOSTS.has(u.hostname.toLowerCase())) {
|
|
401
|
+
throw new FileRefError(
|
|
402
|
+
"not_sprigr_host",
|
|
403
|
+
`${label}.file_url is not a Sprigr file link, and other web addresses are not fetched. ${FILE_REF_SOURCES_HELP}`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
if (path.startsWith(APP_FILES_ROOT)) {
|
|
407
|
+
const own = appKeyFromCallerKey(url, opts.installId);
|
|
408
|
+
if (own.ok) return stored(own.appKey, url, opts);
|
|
409
|
+
return { kind: "url", url, filename: lastSegment(path, "file") };
|
|
410
|
+
}
|
|
411
|
+
if (!opts.companyId || !path.startsWith(`${opts.companyId}/`)) {
|
|
412
|
+
throw new FileRefError("other_company", `${label}.file_url is not a file in this workspace. ${WORKSPACE_FILE_HOW_TO}`);
|
|
413
|
+
}
|
|
414
|
+
if (!u.searchParams.get("token") || !u.searchParams.get("expires")) {
|
|
415
|
+
throw new FileRefError("unsigned_link", `${label}.file_url must be a signed download link (with its token). ${WORKSPACE_FILE_HOW_TO}`);
|
|
416
|
+
}
|
|
417
|
+
return { kind: "url", url, filename: lastSegment(path, "file") };
|
|
418
|
+
}
|
|
419
|
+
if (isWorkspaceKey(key, opts.companyId)) {
|
|
420
|
+
throw new FileRefError("workspace_key", `"${key}" is a file in your own Sprigr storage. ${WORKSPACE_FILE_HOW_TO}`);
|
|
421
|
+
}
|
|
422
|
+
const resolved = appKeyFromCallerKey(key, opts.installId);
|
|
423
|
+
if (!resolved.ok) {
|
|
424
|
+
if (resolved.reason === "other_install") {
|
|
425
|
+
throw new FileRefError(
|
|
426
|
+
"other_install",
|
|
427
|
+
`"${key}" is a file of another app. Pass that app's download_url for it as file_url instead; a file_key only opens inside the app that stored it.`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
throw new FileRefError("malformed_key", callerKeyRefusal(key, resolved));
|
|
431
|
+
}
|
|
432
|
+
return stored(resolved.appKey, key, opts);
|
|
433
|
+
}
|
|
434
|
+
var round1 = (n) => Math.round(n * 10) / 10;
|
|
435
|
+
var human = (n) => n >= 1024 * 1024 ? `${round1(n / (1024 * 1024))} MB` : n >= 1024 ? `${round1(n / 1024)} KB` : `${n} bytes`;
|
|
436
|
+
async function openFileRef(env, ref, opts) {
|
|
437
|
+
const label = opts.label ?? "file";
|
|
438
|
+
const resolved = resolveFileRef(ref, { ...opts, installId: env.INSTALL_ID, companyId: env.COMPANY_ID });
|
|
439
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_FILE_READ_TIMEOUT_MS;
|
|
440
|
+
const tooLarge = (size) => new FileRefError(
|
|
441
|
+
"too_large",
|
|
442
|
+
size === null ? `${label} is over the ${human(opts.maxBytes)} limit, so it was not read.` : `${label} is ${human(size)}, over the ${human(opts.maxBytes)} limit, so it was not read.`
|
|
443
|
+
);
|
|
444
|
+
const failure = (err) => {
|
|
445
|
+
if (err instanceof FileRefError) return err;
|
|
446
|
+
const name = err instanceof Error ? err.name : "";
|
|
447
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
448
|
+
if (name === "TimeoutError" || name === "AbortError") {
|
|
449
|
+
return new FileRefError("timeout", `Reading ${label} took longer than ${Math.round(timeoutMs / 1e3)}s, so it was not read.`);
|
|
450
|
+
}
|
|
451
|
+
return new FileRefError("read_failed", `Could not read ${label}: ${message}`);
|
|
452
|
+
};
|
|
453
|
+
const openUrl = async (url, via, filename) => {
|
|
454
|
+
let res;
|
|
455
|
+
try {
|
|
456
|
+
res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs), redirect: "error" });
|
|
457
|
+
} catch (err) {
|
|
458
|
+
throw failure(err);
|
|
459
|
+
}
|
|
460
|
+
if (!res.ok) {
|
|
461
|
+
await res.body?.cancel().catch(() => {
|
|
462
|
+
});
|
|
463
|
+
if (res.status === 404) throw new FileRefError("not_found", `There is no file at ${label} (the link or key may have expired or been deleted).`);
|
|
464
|
+
throw new FileRefError("read_failed", `Could not read ${label}: the file host answered HTTP ${res.status}.`);
|
|
465
|
+
}
|
|
466
|
+
const declared = Number(res.headers.get("content-length") || "");
|
|
467
|
+
const size = Number.isFinite(declared) && declared >= 0 && res.headers.has("content-length") ? declared : null;
|
|
468
|
+
if (size !== null && size > opts.maxBytes) {
|
|
469
|
+
await res.body?.cancel().catch(() => {
|
|
470
|
+
});
|
|
471
|
+
throw tooLarge(size);
|
|
472
|
+
}
|
|
473
|
+
const contentType = res.headers.get("content-type")?.split(";")[0]?.trim() || "application/octet-stream";
|
|
474
|
+
const reader = (res.body ?? new ReadableStream({ start: (c) => c.close() })).getReader();
|
|
475
|
+
let total = 0;
|
|
476
|
+
const body = new ReadableStream({
|
|
477
|
+
async pull(ctrl) {
|
|
478
|
+
let chunk;
|
|
479
|
+
try {
|
|
480
|
+
chunk = await reader.read();
|
|
481
|
+
} catch (err) {
|
|
482
|
+
ctrl.error(failure(err));
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (chunk.done) {
|
|
486
|
+
ctrl.close();
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
total += chunk.value.byteLength;
|
|
490
|
+
if (total > opts.maxBytes) {
|
|
491
|
+
await reader.cancel().catch(() => {
|
|
492
|
+
});
|
|
493
|
+
ctrl.error(tooLarge(null));
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
ctrl.enqueue(chunk.value);
|
|
497
|
+
},
|
|
498
|
+
cancel(reason) {
|
|
499
|
+
return reader.cancel(reason);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
return { body, size, contentType, filename, via };
|
|
503
|
+
};
|
|
504
|
+
if (resolved.kind === "url") return openUrl(resolved.url, "url", resolved.filename);
|
|
505
|
+
const files = env.SPRIGR?.files;
|
|
506
|
+
if (typeof files?.url === "function") {
|
|
507
|
+
const minted = await files.url(resolved.appKey, { expiresIn: STORED_READ_URL_TTL_SECONDS });
|
|
508
|
+
if (!minted?.url) throw new FileRefError("not_found", `This app's file store has no file at "${resolved.appKey}".`);
|
|
509
|
+
return openUrl(minted.url, "bridge", resolved.filename);
|
|
510
|
+
}
|
|
511
|
+
if (env.SPRIGR_INSTALL_TOKEN && env.SPRIGR_PLATFORM_BASE) {
|
|
512
|
+
let got;
|
|
513
|
+
try {
|
|
514
|
+
got = await getAppFile(env, resolved.appKey);
|
|
515
|
+
} catch (err) {
|
|
516
|
+
throw failure(err);
|
|
517
|
+
}
|
|
518
|
+
if (got.bytes > opts.maxBytes) throw tooLarge(got.bytes);
|
|
519
|
+
const bytes = base64ToBytes(got.base64 ?? "");
|
|
520
|
+
if (bytes.byteLength > opts.maxBytes) throw tooLarge(bytes.byteLength);
|
|
521
|
+
return {
|
|
522
|
+
body: new ReadableStream({
|
|
523
|
+
start(ctrl) {
|
|
524
|
+
ctrl.enqueue(bytes);
|
|
525
|
+
ctrl.close();
|
|
526
|
+
}
|
|
527
|
+
}),
|
|
528
|
+
size: bytes.byteLength,
|
|
529
|
+
contentType: got.contentType || "application/octet-stream",
|
|
530
|
+
filename: got.filename || resolved.filename,
|
|
531
|
+
via: "install_token"
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
throw new FileRefError(
|
|
535
|
+
"no_file_store",
|
|
536
|
+
`This runtime has no file store to read ${label}.file_key from (no env.SPRIGR.files and no install token). Pass a Sprigr file link as file_url instead.`
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
async function readFileRef(env, ref, opts) {
|
|
540
|
+
const opened = await openFileRef(env, ref, opts);
|
|
541
|
+
const chunks = [];
|
|
542
|
+
let size = 0;
|
|
543
|
+
const reader = opened.body.getReader();
|
|
544
|
+
for (; ; ) {
|
|
545
|
+
const { done, value } = await reader.read();
|
|
546
|
+
if (done) break;
|
|
547
|
+
chunks.push(value);
|
|
548
|
+
size += value.byteLength;
|
|
549
|
+
}
|
|
550
|
+
const bytes = chunks.length === 1 ? chunks[0] : new Uint8Array(size);
|
|
551
|
+
if (chunks.length !== 1) {
|
|
552
|
+
let at = 0;
|
|
553
|
+
for (const c of chunks) {
|
|
554
|
+
bytes.set(c, at);
|
|
555
|
+
at += c.byteLength;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return { bytes, size, contentType: opened.contentType, filename: opened.filename, via: opened.via };
|
|
559
|
+
}
|
|
560
|
+
|
|
158
561
|
// src/platform-host.ts
|
|
159
562
|
var FALLBACK_PLATFORM_BASE = "https://webhooks.sprigr.com";
|
|
160
563
|
function resolvePlatformWebhookBase(env, override) {
|
|
@@ -864,14 +1267,14 @@ function parseActorRetiredArgs(args) {
|
|
|
864
1267
|
throw new Error("on_actor_retired: refused, the call carries an actor; only the platform hook may call this");
|
|
865
1268
|
}
|
|
866
1269
|
if (b.trigger !== "actor_retired") throw new Error(`on_actor_retired: trigger must be actor_retired, got ${String(b.trigger)}`);
|
|
867
|
-
const
|
|
1270
|
+
const str2 = (k) => {
|
|
868
1271
|
const v = b[k];
|
|
869
1272
|
if (typeof v !== "string" || v === "") throw new Error(`on_actor_retired: ${k} is required`);
|
|
870
1273
|
return v;
|
|
871
1274
|
};
|
|
872
|
-
const installId =
|
|
873
|
-
const companyId =
|
|
874
|
-
const key =
|
|
1275
|
+
const installId = str2("install_id");
|
|
1276
|
+
const companyId = str2("company_id");
|
|
1277
|
+
const key = str2("actor_key");
|
|
875
1278
|
const m = KEY_RE.exec(key);
|
|
876
1279
|
if (!m) throw new Error(`on_actor_retired: actor_key must be u:<id> or a:<id>, got ${key.slice(0, 80)}`);
|
|
877
1280
|
const actor = m[1] === "u" ? { platformUserId: m[2] } : { agentId: m[2] };
|
|
@@ -886,9 +1289,9 @@ function parseActorRetiredArgs(args) {
|
|
|
886
1289
|
if (typeof reason !== "string" || !ACTOR_RETIRE_REASONS.includes(reason)) {
|
|
887
1290
|
throw new Error(`on_actor_retired: unknown reason ${String(reason)}`);
|
|
888
1291
|
}
|
|
889
|
-
const storageOwnerRef =
|
|
1292
|
+
const storageOwnerRef = str2("storage_owner_ref");
|
|
890
1293
|
if (!OWNER_REF_RE.test(storageOwnerRef)) throw new Error("on_actor_retired: storage_owner_ref must be 32 lowercase hex characters");
|
|
891
|
-
const requestedAt = Date.parse(
|
|
1294
|
+
const requestedAt = Date.parse(str2("requested_at"));
|
|
892
1295
|
if (!Number.isFinite(requestedAt)) throw new Error("on_actor_retired: requested_at is not a date");
|
|
893
1296
|
const attempt = typeof b.attempt === "number" && b.attempt >= 1 ? b.attempt : 1;
|
|
894
1297
|
return {
|
|
@@ -915,118 +1318,6 @@ function actorRetiredHook(fn) {
|
|
|
915
1318
|
};
|
|
916
1319
|
}
|
|
917
1320
|
|
|
918
|
-
// src/app-files.ts
|
|
919
|
-
var DEFAULT_PLATFORM_BASE = "https://webhooks.sprigr.com";
|
|
920
|
-
function platformBase(env) {
|
|
921
|
-
return (env.SPRIGR_PLATFORM_BASE ?? DEFAULT_PLATFORM_BASE).trim().replace(/\/+$/, "");
|
|
922
|
-
}
|
|
923
|
-
var AppFilesError = class extends Error {
|
|
924
|
-
status;
|
|
925
|
-
constructor(message, status) {
|
|
926
|
-
super(message);
|
|
927
|
-
this.name = "AppFilesError";
|
|
928
|
-
this.status = status;
|
|
929
|
-
}
|
|
930
|
-
};
|
|
931
|
-
function requireToken(env) {
|
|
932
|
-
const token = env.SPRIGR_INSTALL_TOKEN;
|
|
933
|
-
if (!token) {
|
|
934
|
-
throw new AppFilesError("app file storage unavailable: SPRIGR_INSTALL_TOKEN unset", 500);
|
|
935
|
-
}
|
|
936
|
-
return token;
|
|
937
|
-
}
|
|
938
|
-
async function parseResult(res, op) {
|
|
939
|
-
const text = await res.text();
|
|
940
|
-
let parsed = void 0;
|
|
941
|
-
try {
|
|
942
|
-
parsed = text ? JSON.parse(text) : void 0;
|
|
943
|
-
} catch {
|
|
944
|
-
}
|
|
945
|
-
if (!res.ok) {
|
|
946
|
-
const detail = parsed && typeof parsed === "object" ? parsed.hint ?? parsed.detail ?? parsed.error : text.slice(0, 200);
|
|
947
|
-
throw new AppFilesError(`app file ${op} failed (${res.status}): ${String(detail)}`, res.status);
|
|
948
|
-
}
|
|
949
|
-
return parsed;
|
|
950
|
-
}
|
|
951
|
-
async function call(env, op, body) {
|
|
952
|
-
const token = requireToken(env);
|
|
953
|
-
const res = await fetch(`${platformBase(env)}/internal/wfp/file/${op}`, {
|
|
954
|
-
method: "POST",
|
|
955
|
-
headers: {
|
|
956
|
-
authorization: `Bearer ${token}`,
|
|
957
|
-
"content-type": "application/json"
|
|
958
|
-
},
|
|
959
|
-
body: JSON.stringify(body ?? {})
|
|
960
|
-
});
|
|
961
|
-
return parseResult(res, op);
|
|
962
|
-
}
|
|
963
|
-
function putAppFile(env, args) {
|
|
964
|
-
return call(env, "put", {
|
|
965
|
-
key: args.key,
|
|
966
|
-
base64: args.base64,
|
|
967
|
-
contentType: args.contentType,
|
|
968
|
-
filename: args.filename
|
|
969
|
-
});
|
|
970
|
-
}
|
|
971
|
-
var STREAM_PUT_MAX_ATTEMPTS = 3;
|
|
972
|
-
var STREAM_PUT_BASE_DELAY_MS = 500;
|
|
973
|
-
function isTransientPutStreamFailure(status, message) {
|
|
974
|
-
if (status === 503) return true;
|
|
975
|
-
if (status !== 502) return false;
|
|
976
|
-
const msg = message.toLowerCase();
|
|
977
|
-
return msg.includes("(10001)") || msg.includes("(10043)");
|
|
978
|
-
}
|
|
979
|
-
function sleep2(ms) {
|
|
980
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
981
|
-
}
|
|
982
|
-
async function putAppFileStream(env, args) {
|
|
983
|
-
const token = requireToken(env);
|
|
984
|
-
const headers = {
|
|
985
|
-
authorization: `Bearer ${token}`,
|
|
986
|
-
"content-type": args.contentType || "application/octet-stream",
|
|
987
|
-
"x-app-file-key": args.key,
|
|
988
|
-
"x-app-file-content-type": args.contentType || "application/octet-stream"
|
|
989
|
-
};
|
|
990
|
-
if (args.filename) headers["x-app-file-name"] = encodeURIComponent(args.filename);
|
|
991
|
-
if (typeof args.contentLength === "number" && Number.isFinite(args.contentLength)) {
|
|
992
|
-
headers["x-app-file-length"] = String(args.contentLength);
|
|
993
|
-
}
|
|
994
|
-
const resendable = !(args.body instanceof ReadableStream);
|
|
995
|
-
const attempts = resendable ? STREAM_PUT_MAX_ATTEMPTS : 1;
|
|
996
|
-
let lastErr;
|
|
997
|
-
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
998
|
-
const res = await fetch(`${platformBase(env)}/internal/wfp/file/put-stream`, {
|
|
999
|
-
method: "POST",
|
|
1000
|
-
headers,
|
|
1001
|
-
body: args.body,
|
|
1002
|
-
// Required by the Workers runtime when sending a streaming request body.
|
|
1003
|
-
...args.body instanceof ReadableStream ? { duplex: "half" } : {}
|
|
1004
|
-
});
|
|
1005
|
-
try {
|
|
1006
|
-
return await parseResult(res, "put-stream");
|
|
1007
|
-
} catch (err) {
|
|
1008
|
-
lastErr = err;
|
|
1009
|
-
if (!resendable || attempt === attempts || !(err instanceof AppFilesError) || !isTransientPutStreamFailure(err.status, err.message)) {
|
|
1010
|
-
throw err;
|
|
1011
|
-
}
|
|
1012
|
-
await sleep2(STREAM_PUT_BASE_DELAY_MS * 2 ** (attempt - 1));
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
throw lastErr;
|
|
1016
|
-
}
|
|
1017
|
-
function appFileUrl(env, args) {
|
|
1018
|
-
return call(env, "url", { key: args.key, expiresIn: args.expiresIn });
|
|
1019
|
-
}
|
|
1020
|
-
function getAppFile(env, key) {
|
|
1021
|
-
return call(env, "get", { key });
|
|
1022
|
-
}
|
|
1023
|
-
function listAppFiles(env, prefix) {
|
|
1024
|
-
return call(env, "list", { prefix });
|
|
1025
|
-
}
|
|
1026
|
-
function deleteAppFile(env, key) {
|
|
1027
|
-
return call(env, "delete", { key });
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
1321
|
// src/write-protection/types.ts
|
|
1031
1322
|
var APPROVAL_GRANTED_KEY = "_approval_granted";
|
|
1032
1323
|
|
|
@@ -1367,6 +1658,13 @@ async function runUndoApply(args, opts) {
|
|
|
1367
1658
|
}
|
|
1368
1659
|
|
|
1369
1660
|
// src/write-protection/confirmation-policy.ts
|
|
1661
|
+
function coverageSets(c) {
|
|
1662
|
+
if (!c) return null;
|
|
1663
|
+
if (c.covered == null || c.conditional == null) {
|
|
1664
|
+
throw new Error("approval coverage needs both `covered` and `conditional` (pass [] when no action is gated conditionally)");
|
|
1665
|
+
}
|
|
1666
|
+
return { covered: new Set(c.covered), conditional: new Set(c.conditional) };
|
|
1667
|
+
}
|
|
1370
1668
|
function buildConfirmationPolicy(src) {
|
|
1371
1669
|
const actions = {};
|
|
1372
1670
|
const put = (name, rule, group) => {
|
|
@@ -1376,6 +1674,10 @@ function buildConfirmationPolicy(src) {
|
|
|
1376
1674
|
for (const [name, describe] of Object.entries(src.irreversible ?? {})) put(name, { always: true, describe, irreversible: true }, "irreversible");
|
|
1377
1675
|
for (const [name, describe] of Object.entries(src.always ?? {})) put(name, { always: true, describe }, "always");
|
|
1378
1676
|
for (const [name, rule] of Object.entries(src.rules ?? {})) put(name, rule, "rules");
|
|
1677
|
+
const cov = coverageSets(src.approval);
|
|
1678
|
+
for (const name of cov?.covered ?? []) {
|
|
1679
|
+
if (actions[name] && !cov?.conditional.has(name)) actions[name] = { ...actions[name], attended: "approval_card" };
|
|
1680
|
+
}
|
|
1379
1681
|
return { actions: Object.fromEntries(Object.entries(actions).sort(([a], [b]) => a.localeCompare(b))) };
|
|
1380
1682
|
}
|
|
1381
1683
|
var DEFAULT_WRITE_PREFIXES = [
|
|
@@ -1479,6 +1781,22 @@ var PLACEHOLDER = /\{([a-zA-Z0-9_.]+)\}/g;
|
|
|
1479
1781
|
function conditions(rule) {
|
|
1480
1782
|
return !rule.when ? [] : Array.isArray(rule.when) ? rule.when : [rule.when];
|
|
1481
1783
|
}
|
|
1784
|
+
function attendedFindings(name, rule, coverage) {
|
|
1785
|
+
const cov = coverageSets(coverage ?? void 0);
|
|
1786
|
+
const out = [];
|
|
1787
|
+
const attended = rule.attended;
|
|
1788
|
+
const gated = rule.always === true || conditions(rule).length > 0;
|
|
1789
|
+
if (attended !== void 0 && attended !== "approval_card") {
|
|
1790
|
+
out.push(`${name}: attended must be 'approval_card' (got ${JSON.stringify(attended)}); the platform ignores anything else`);
|
|
1791
|
+
} else if (attended === "approval_card") {
|
|
1792
|
+
if (!cov) out.push(`${name}: attended: 'approval_card' cannot be checked; pass approval: { covered, conditional }`);
|
|
1793
|
+
else if (!cov.covered.has(name)) out.push(`${name}: attended: 'approval_card' but no approval spec covers it, so it removes the only prompt`);
|
|
1794
|
+
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`);
|
|
1795
|
+
} else if (cov?.covered.has(name) && !cov.conditional.has(name) && gated) {
|
|
1796
|
+
out.push(`${name}: an approval spec covers it but the rule lacks attended: 'approval_card', so it asks twice`);
|
|
1797
|
+
}
|
|
1798
|
+
return out;
|
|
1799
|
+
}
|
|
1482
1800
|
function checkConfirmationPolicy(input) {
|
|
1483
1801
|
const registry = new Set(input.registry);
|
|
1484
1802
|
const ungated = input.ungated ?? [];
|
|
@@ -1486,8 +1804,13 @@ function checkConfirmationPolicy(input) {
|
|
|
1486
1804
|
const nested = input.nestedUnderInput ?? true;
|
|
1487
1805
|
const money = new Set(input.moneyFields ?? DEFAULT_MONEY_FIELDS);
|
|
1488
1806
|
const actions = input.policy.actions ?? {};
|
|
1807
|
+
const cov = coverageSets(input.approval);
|
|
1489
1808
|
const out = [];
|
|
1809
|
+
for (const name of cov?.conditional ?? []) {
|
|
1810
|
+
if (!cov?.covered.has(name)) out.push(`approval.conditional: "${name}" has no approval spec (not in covered)`);
|
|
1811
|
+
}
|
|
1490
1812
|
for (const [name, rule] of Object.entries(actions)) {
|
|
1813
|
+
out.push(...attendedFindings(name, rule, input.approval ?? null));
|
|
1491
1814
|
if (!registry.has(name)) out.push(`dead rule: "${name}" is not in the registry, so it gates nothing`);
|
|
1492
1815
|
if (!rule.always && conditions(rule).length === 0) out.push(`${name}: rule says nothing (no always, no when)`);
|
|
1493
1816
|
if (!rule.describe || !rule.describe.trim()) out.push(`${name}: describe missing`);
|
|
@@ -1596,7 +1919,7 @@ function deriveConfirmationPolicy(registry, opts) {
|
|
|
1596
1919
|
}
|
|
1597
1920
|
if (!hit) unmatched.push(name);
|
|
1598
1921
|
}
|
|
1599
|
-
const policy = buildConfirmationPolicy({ irreversible, always, rules: overrides });
|
|
1922
|
+
const policy = buildConfirmationPolicy({ irreversible, always, rules: overrides, approval: opts.approval });
|
|
1600
1923
|
return { policy, ungated, unmatched };
|
|
1601
1924
|
}
|
|
1602
1925
|
function commonDestructiveVerbs(subject) {
|
|
@@ -1718,82 +2041,6 @@ async function connectDisconnect(env, req) {
|
|
|
1718
2041
|
if (!bridge) throw new Error("connectDisconnect: SPRIGR_PLATFORM_BASE / SPRIGR_INSTALL_TOKEN are not bound");
|
|
1719
2042
|
return await installTokenPost(bridge, "/internal/wfp/connect/disconnect", req, { label: "connect disconnect" });
|
|
1720
2043
|
}
|
|
1721
|
-
|
|
1722
|
-
// src/stored-file-keys.ts
|
|
1723
|
-
var APP_FILES_ROOT = "_apps/";
|
|
1724
|
-
var APP_OWNER_SEGMENT = "~u";
|
|
1725
|
-
function isSegment(s) {
|
|
1726
|
-
return typeof s === "string" && s.length > 0 && !s.includes("/");
|
|
1727
|
-
}
|
|
1728
|
-
function trimLeadingSlashes(key) {
|
|
1729
|
-
return key.replace(/^\/+/, "");
|
|
1730
|
-
}
|
|
1731
|
-
function parseAbsolute(key) {
|
|
1732
|
-
if (!key.startsWith(APP_FILES_ROOT)) return null;
|
|
1733
|
-
const parts = key.slice(APP_FILES_ROOT.length).split("/");
|
|
1734
|
-
const installId = parts[0];
|
|
1735
|
-
if (!isSegment(installId)) return null;
|
|
1736
|
-
let i = 1;
|
|
1737
|
-
let ownerHash = null;
|
|
1738
|
-
if (parts[1] === APP_OWNER_SEGMENT) {
|
|
1739
|
-
if (!isSegment(parts[2])) return null;
|
|
1740
|
-
ownerHash = parts[2];
|
|
1741
|
-
i = 3;
|
|
1742
|
-
}
|
|
1743
|
-
const rest = parts.slice(i).join("/");
|
|
1744
|
-
if (!rest || rest.split("/").some((seg) => seg === "")) return null;
|
|
1745
|
-
return { installId, ownerHash, rest };
|
|
1746
|
-
}
|
|
1747
|
-
function urlPathKey(url) {
|
|
1748
|
-
try {
|
|
1749
|
-
return trimLeadingSlashes(decodeURIComponent(new URL(url).pathname));
|
|
1750
|
-
} catch {
|
|
1751
|
-
return null;
|
|
1752
|
-
}
|
|
1753
|
-
}
|
|
1754
|
-
function agentFileKeyFromUrl(downloadUrl, appKey) {
|
|
1755
|
-
const rel = trimLeadingSlashes(appKey);
|
|
1756
|
-
if (!rel) return null;
|
|
1757
|
-
const path = urlPathKey(downloadUrl);
|
|
1758
|
-
if (!path) return null;
|
|
1759
|
-
const parsed = parseAbsolute(path);
|
|
1760
|
-
if (!parsed || parsed.rest !== rel) return null;
|
|
1761
|
-
return path;
|
|
1762
|
-
}
|
|
1763
|
-
function readableStoredKey(appKey, downloadUrl) {
|
|
1764
|
-
return agentFileKeyFromUrl(downloadUrl, appKey) ?? appKey;
|
|
1765
|
-
}
|
|
1766
|
-
function appKeyFromCallerKey(key, ownInstallId) {
|
|
1767
|
-
let candidate = key.trim();
|
|
1768
|
-
let wasAbsolute = false;
|
|
1769
|
-
if (/^https?:\/\//i.test(candidate)) {
|
|
1770
|
-
const path = urlPathKey(candidate);
|
|
1771
|
-
if (!path || !path.startsWith(APP_FILES_ROOT)) return { ok: false, reason: "malformed" };
|
|
1772
|
-
candidate = path;
|
|
1773
|
-
}
|
|
1774
|
-
candidate = trimLeadingSlashes(candidate);
|
|
1775
|
-
if (candidate.startsWith(APP_FILES_ROOT)) {
|
|
1776
|
-
const parsed = parseAbsolute(candidate);
|
|
1777
|
-
if (!parsed) return { ok: false, reason: "malformed" };
|
|
1778
|
-
if (ownInstallId && parsed.installId !== ownInstallId) {
|
|
1779
|
-
return { ok: false, reason: "other_install", installId: parsed.installId };
|
|
1780
|
-
}
|
|
1781
|
-
return { ok: true, appKey: parsed.rest, wasAbsolute: true };
|
|
1782
|
-
}
|
|
1783
|
-
const parts = candidate.split("/");
|
|
1784
|
-
if (parts[0] === APP_OWNER_SEGMENT) {
|
|
1785
|
-
const rest = parts.slice(2).join("/");
|
|
1786
|
-
if (!isSegment(parts[1]) || !rest) return { ok: false, reason: "malformed" };
|
|
1787
|
-
return { ok: true, appKey: rest, wasAbsolute: true };
|
|
1788
|
-
}
|
|
1789
|
-
return { ok: true, appKey: candidate, wasAbsolute };
|
|
1790
|
-
}
|
|
1791
|
-
function callerKeyRefusal(key, r) {
|
|
1792
|
-
if (r.reason === "other_install") {
|
|
1793
|
-
return `"${key}" is a stored file of a different app install (${r.installId}), not this one. Pass a file_key this app returned to you.`;
|
|
1794
|
-
}
|
|
1795
|
-
return `"${key}" is not a stored file key: it has no file path after the app storage prefix.`;
|
|
1796
|
-
}
|
|
1797
2044
|
export {
|
|
1798
2045
|
ACTOR_RETIRED_HOOK_TOOL,
|
|
1799
2046
|
ACTOR_RETIRE_REASONS,
|
|
@@ -1803,15 +2050,19 @@ export {
|
|
|
1803
2050
|
APP_OWNER_SEGMENT,
|
|
1804
2051
|
DATA_PARTIAL_UPDATE_PATH,
|
|
1805
2052
|
DEFAULT_EMIT_TIMEOUT_MS,
|
|
2053
|
+
DEFAULT_FILE_READ_TIMEOUT_MS,
|
|
1806
2054
|
DEFAULT_LOG_TIMEOUT_MS,
|
|
1807
2055
|
DEFAULT_MAX_FILE_BYTES,
|
|
1808
2056
|
DEFAULT_MONEY_FIELDS,
|
|
1809
2057
|
DEFAULT_WRITE_PREFIXES,
|
|
2058
|
+
FILE_REF_SOURCES_HELP,
|
|
2059
|
+
FileRefError,
|
|
1810
2060
|
INBOX_SEND_CONTRACT_V1,
|
|
1811
2061
|
InboxSendRefusal,
|
|
1812
2062
|
InvalidUndoEnvelopeError,
|
|
1813
2063
|
NotConnectedError,
|
|
1814
2064
|
SPRIGR_DATA_MAX_OBJECTS_PER_CALL,
|
|
2065
|
+
SPRIGR_FILE_HOSTS,
|
|
1815
2066
|
SPRIGR_LOG_LEVELS,
|
|
1816
2067
|
SPRIGR_LOG_LIMITS,
|
|
1817
2068
|
SprigrDataValidationError,
|
|
@@ -1822,6 +2073,7 @@ export {
|
|
|
1822
2073
|
VIEWER_PLATFORM_USER_ID_HEADER,
|
|
1823
2074
|
VIEWER_ROLE_HEADER,
|
|
1824
2075
|
VIEWER_USER_ID_HEADER,
|
|
2076
|
+
WORKSPACE_FILE_HOW_TO,
|
|
1825
2077
|
actorKey,
|
|
1826
2078
|
actorRetiredHook,
|
|
1827
2079
|
actorTool,
|
|
@@ -1833,6 +2085,7 @@ export {
|
|
|
1833
2085
|
approvalPreview,
|
|
1834
2086
|
approvalRecipients,
|
|
1835
2087
|
archiveOfferRefusal,
|
|
2088
|
+
attendedFindings,
|
|
1836
2089
|
base64ToBytes,
|
|
1837
2090
|
buildConfirmationPolicy,
|
|
1838
2091
|
buildMarketplaceWebhookUrl,
|
|
@@ -1870,6 +2123,7 @@ export {
|
|
|
1870
2123
|
logToPlatform,
|
|
1871
2124
|
manifestIsFresh,
|
|
1872
2125
|
offerUndo,
|
|
2126
|
+
openFileRef,
|
|
1873
2127
|
overlaySprigr,
|
|
1874
2128
|
ownerFromActorKey,
|
|
1875
2129
|
parseActor,
|
|
@@ -1878,10 +2132,12 @@ export {
|
|
|
1878
2132
|
putAppFile,
|
|
1879
2133
|
putAppFileStream,
|
|
1880
2134
|
randomHex,
|
|
2135
|
+
readFileRef,
|
|
1881
2136
|
readableStoredKey,
|
|
1882
2137
|
recreatedWarning,
|
|
1883
2138
|
refuseWithoutForce,
|
|
1884
2139
|
requireApproval,
|
|
2140
|
+
resolveFileRef,
|
|
1885
2141
|
resolveInstallBridge,
|
|
1886
2142
|
resolveOAuthBouncerBase,
|
|
1887
2143
|
resolveOAuthBouncerCallbackUrl,
|