@sprigr/apps-app-sdk 0.20.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 +1 -0
- package/dist/index.d.ts +251 -119
- package/dist/index.js +417 -194
- 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
|
|
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
|
*
|
|
@@ -3207,4 +3339,4 @@ interface ExecutionContextLike {
|
|
|
3207
3339
|
}
|
|
3208
3340
|
type HandlerFn<TArgs, TResult> = (args: TArgs, env: Record<string, unknown>, ctx?: ExecutionContextLike) => Promise<TResult>;
|
|
3209
3341
|
|
|
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 };
|
|
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
|
|
|
@@ -1750,82 +2041,6 @@ async function connectDisconnect(env, req) {
|
|
|
1750
2041
|
if (!bridge) throw new Error("connectDisconnect: SPRIGR_PLATFORM_BASE / SPRIGR_INSTALL_TOKEN are not bound");
|
|
1751
2042
|
return await installTokenPost(bridge, "/internal/wfp/connect/disconnect", req, { label: "connect disconnect" });
|
|
1752
2043
|
}
|
|
1753
|
-
|
|
1754
|
-
// src/stored-file-keys.ts
|
|
1755
|
-
var APP_FILES_ROOT = "_apps/";
|
|
1756
|
-
var APP_OWNER_SEGMENT = "~u";
|
|
1757
|
-
function isSegment(s) {
|
|
1758
|
-
return typeof s === "string" && s.length > 0 && !s.includes("/");
|
|
1759
|
-
}
|
|
1760
|
-
function trimLeadingSlashes(key) {
|
|
1761
|
-
return key.replace(/^\/+/, "");
|
|
1762
|
-
}
|
|
1763
|
-
function parseAbsolute(key) {
|
|
1764
|
-
if (!key.startsWith(APP_FILES_ROOT)) return null;
|
|
1765
|
-
const parts = key.slice(APP_FILES_ROOT.length).split("/");
|
|
1766
|
-
const installId = parts[0];
|
|
1767
|
-
if (!isSegment(installId)) return null;
|
|
1768
|
-
let i = 1;
|
|
1769
|
-
let ownerHash = null;
|
|
1770
|
-
if (parts[1] === APP_OWNER_SEGMENT) {
|
|
1771
|
-
if (!isSegment(parts[2])) return null;
|
|
1772
|
-
ownerHash = parts[2];
|
|
1773
|
-
i = 3;
|
|
1774
|
-
}
|
|
1775
|
-
const rest = parts.slice(i).join("/");
|
|
1776
|
-
if (!rest || rest.split("/").some((seg) => seg === "")) return null;
|
|
1777
|
-
return { installId, ownerHash, rest };
|
|
1778
|
-
}
|
|
1779
|
-
function urlPathKey(url) {
|
|
1780
|
-
try {
|
|
1781
|
-
return trimLeadingSlashes(decodeURIComponent(new URL(url).pathname));
|
|
1782
|
-
} catch {
|
|
1783
|
-
return null;
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
function agentFileKeyFromUrl(downloadUrl, appKey) {
|
|
1787
|
-
const rel = trimLeadingSlashes(appKey);
|
|
1788
|
-
if (!rel) return null;
|
|
1789
|
-
const path = urlPathKey(downloadUrl);
|
|
1790
|
-
if (!path) return null;
|
|
1791
|
-
const parsed = parseAbsolute(path);
|
|
1792
|
-
if (!parsed || parsed.rest !== rel) return null;
|
|
1793
|
-
return path;
|
|
1794
|
-
}
|
|
1795
|
-
function readableStoredKey(appKey, downloadUrl) {
|
|
1796
|
-
return agentFileKeyFromUrl(downloadUrl, appKey) ?? appKey;
|
|
1797
|
-
}
|
|
1798
|
-
function appKeyFromCallerKey(key, ownInstallId) {
|
|
1799
|
-
let candidate = key.trim();
|
|
1800
|
-
let wasAbsolute = false;
|
|
1801
|
-
if (/^https?:\/\//i.test(candidate)) {
|
|
1802
|
-
const path = urlPathKey(candidate);
|
|
1803
|
-
if (!path || !path.startsWith(APP_FILES_ROOT)) return { ok: false, reason: "malformed" };
|
|
1804
|
-
candidate = path;
|
|
1805
|
-
}
|
|
1806
|
-
candidate = trimLeadingSlashes(candidate);
|
|
1807
|
-
if (candidate.startsWith(APP_FILES_ROOT)) {
|
|
1808
|
-
const parsed = parseAbsolute(candidate);
|
|
1809
|
-
if (!parsed) return { ok: false, reason: "malformed" };
|
|
1810
|
-
if (ownInstallId && parsed.installId !== ownInstallId) {
|
|
1811
|
-
return { ok: false, reason: "other_install", installId: parsed.installId };
|
|
1812
|
-
}
|
|
1813
|
-
return { ok: true, appKey: parsed.rest, wasAbsolute: true };
|
|
1814
|
-
}
|
|
1815
|
-
const parts = candidate.split("/");
|
|
1816
|
-
if (parts[0] === APP_OWNER_SEGMENT) {
|
|
1817
|
-
const rest = parts.slice(2).join("/");
|
|
1818
|
-
if (!isSegment(parts[1]) || !rest) return { ok: false, reason: "malformed" };
|
|
1819
|
-
return { ok: true, appKey: rest, wasAbsolute: true };
|
|
1820
|
-
}
|
|
1821
|
-
return { ok: true, appKey: candidate, wasAbsolute };
|
|
1822
|
-
}
|
|
1823
|
-
function callerKeyRefusal(key, r) {
|
|
1824
|
-
if (r.reason === "other_install") {
|
|
1825
|
-
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.`;
|
|
1826
|
-
}
|
|
1827
|
-
return `"${key}" is not a stored file key: it has no file path after the app storage prefix.`;
|
|
1828
|
-
}
|
|
1829
2044
|
export {
|
|
1830
2045
|
ACTOR_RETIRED_HOOK_TOOL,
|
|
1831
2046
|
ACTOR_RETIRE_REASONS,
|
|
@@ -1835,15 +2050,19 @@ export {
|
|
|
1835
2050
|
APP_OWNER_SEGMENT,
|
|
1836
2051
|
DATA_PARTIAL_UPDATE_PATH,
|
|
1837
2052
|
DEFAULT_EMIT_TIMEOUT_MS,
|
|
2053
|
+
DEFAULT_FILE_READ_TIMEOUT_MS,
|
|
1838
2054
|
DEFAULT_LOG_TIMEOUT_MS,
|
|
1839
2055
|
DEFAULT_MAX_FILE_BYTES,
|
|
1840
2056
|
DEFAULT_MONEY_FIELDS,
|
|
1841
2057
|
DEFAULT_WRITE_PREFIXES,
|
|
2058
|
+
FILE_REF_SOURCES_HELP,
|
|
2059
|
+
FileRefError,
|
|
1842
2060
|
INBOX_SEND_CONTRACT_V1,
|
|
1843
2061
|
InboxSendRefusal,
|
|
1844
2062
|
InvalidUndoEnvelopeError,
|
|
1845
2063
|
NotConnectedError,
|
|
1846
2064
|
SPRIGR_DATA_MAX_OBJECTS_PER_CALL,
|
|
2065
|
+
SPRIGR_FILE_HOSTS,
|
|
1847
2066
|
SPRIGR_LOG_LEVELS,
|
|
1848
2067
|
SPRIGR_LOG_LIMITS,
|
|
1849
2068
|
SprigrDataValidationError,
|
|
@@ -1854,6 +2073,7 @@ export {
|
|
|
1854
2073
|
VIEWER_PLATFORM_USER_ID_HEADER,
|
|
1855
2074
|
VIEWER_ROLE_HEADER,
|
|
1856
2075
|
VIEWER_USER_ID_HEADER,
|
|
2076
|
+
WORKSPACE_FILE_HOW_TO,
|
|
1857
2077
|
actorKey,
|
|
1858
2078
|
actorRetiredHook,
|
|
1859
2079
|
actorTool,
|
|
@@ -1903,6 +2123,7 @@ export {
|
|
|
1903
2123
|
logToPlatform,
|
|
1904
2124
|
manifestIsFresh,
|
|
1905
2125
|
offerUndo,
|
|
2126
|
+
openFileRef,
|
|
1906
2127
|
overlaySprigr,
|
|
1907
2128
|
ownerFromActorKey,
|
|
1908
2129
|
parseActor,
|
|
@@ -1911,10 +2132,12 @@ export {
|
|
|
1911
2132
|
putAppFile,
|
|
1912
2133
|
putAppFileStream,
|
|
1913
2134
|
randomHex,
|
|
2135
|
+
readFileRef,
|
|
1914
2136
|
readableStoredKey,
|
|
1915
2137
|
recreatedWarning,
|
|
1916
2138
|
refuseWithoutForce,
|
|
1917
2139
|
requireApproval,
|
|
2140
|
+
resolveFileRef,
|
|
1918
2141
|
resolveInstallBridge,
|
|
1919
2142
|
resolveOAuthBouncerBase,
|
|
1920
2143
|
resolveOAuthBouncerCallbackUrl,
|