@sprigr/apps-app-sdk 0.15.2 → 0.17.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 +23 -0
- package/dist/index.d.ts +181 -1
- package/dist/index.js +154 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,6 +44,28 @@ export default { list_things: actorTool(async (env, actor, args) => { /* ... */
|
|
|
44
44
|
|
|
45
45
|
**Why this is in the SDK:** every app hand-rolled this, and one shipped a version that fell back to "the first connected actor on the install" when the caller had none. One person's consent then exposed their account to every agent on that install, in production.
|
|
46
46
|
|
|
47
|
+
## Retiring a person or agent: `on_actor_retired`
|
|
48
|
+
|
|
49
|
+
A per-actor app must forget a person when the platform does. When a member is removed, an agent is deleted, a companion's bound user id is corrected, or an operator retires an orphaned key, the platform POSTs `/__sprigr/tool/on_actor_retired` to every install that declares a tool of that name (sprigr-team decision 0154). Run the same revoke-and-purge your own disconnect tool runs, for that key only.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { actorRetiredHook } from '@sprigr/apps-app-sdk';
|
|
53
|
+
|
|
54
|
+
export default {
|
|
55
|
+
on_actor_retired: actorRetiredHook<MyEnv>(async (env, retired) => {
|
|
56
|
+
// retired.actor is shaped like a stamped actor: actorKey(retired.actor) === retired.actorKey
|
|
57
|
+
await revokeAndPurge(env, retired.actor, { cutoff: retired.requestedAt, ownerRef: retired.storageOwnerRef });
|
|
58
|
+
return { done: true };
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- Declare it in `tools[]` with `"internal": true`. The platform never lets an agent or the app bridge call it.
|
|
64
|
+
- `actorRetiredHook` refuses a call that carries `args.actor` (the platform's dispatch never does) and a malformed body, by throwing. It never turns a throw into `{ ok: false }`: the platform reads `done`, and a swallowed error would read as finished. A throw is retried.
|
|
65
|
+
- Return `{ done: true }` when finished or durably queued by the app; `{ done: false, remaining }` to be called again (1 min up to 12 h, 8 deliveries). Be idempotent.
|
|
66
|
+
- `successorKey` is audit only: never move the grant or the data to it. Delete only what existed by `requestedAt`.
|
|
67
|
+
- `storageOwnerRef` is the `owner_ref` a no-actor maintenance `env.SPRIGR.files.list` reports for this person's per-user stored files; pass it to a no-actor `files.delete(key, { owner_ref })`.
|
|
68
|
+
|
|
47
69
|
## Webhook callback URLs
|
|
48
70
|
|
|
49
71
|
- `resolvePlatformWebhookBase(env)` / `buildMarketplaceWebhookUrl(env, installId, topicPath)` — env-correct platform host, so a staging install never registers prod-pointing subscriptions.
|
|
@@ -56,6 +78,7 @@ export default { list_things: actorTool(async (env, actor, args) => { /* ... */
|
|
|
56
78
|
- `parseActor` / `actorKey` / `ownerFromActorKey` — per-actor token scoping
|
|
57
79
|
- `putAppFile` / `putAppFileStream` / `appFileUrl` / `getAppFile` / `listAppFiles` / `deleteAppFile` — durable app-scoped file storage from outside the injected bridge
|
|
58
80
|
- `fetchFileBytes` / `fetchFileAsBase64` / `bytesToBase64` / `base64ToBytes` — file byte helpers
|
|
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)
|
|
59
82
|
|
|
60
83
|
Full platform semantics: [`docs/platform-reference.md`](../../docs/platform-reference.md).
|
|
61
84
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1185,6 +1185,113 @@ declare function createToolWrappers<Env>(opts?: ToolWrapperOptions): {
|
|
|
1185
1185
|
actorTool: <A, R>(fn: (env: Env, actor: Actor, args: A) => Promise<R>) => (args: A, env: Env) => Promise<ToolResult<R>>;
|
|
1186
1186
|
};
|
|
1187
1187
|
|
|
1188
|
+
/**
|
|
1189
|
+
* The `on_actor_retired` lifecycle hook (sprigr-team decision 0154,
|
|
1190
|
+
* sprigr/sprigr-team#10109).
|
|
1191
|
+
*
|
|
1192
|
+
* A per-actor app keys a person's connection, tokens and data on the key
|
|
1193
|
+
* `actorKey()` derives: `u:<platformUserId>`, else `a:<agentId>`. When that
|
|
1194
|
+
* identity goes away (a member is removed, an agent is deleted, a companion's
|
|
1195
|
+
* bound user id is corrected, or an operator retires an orphan), the platform
|
|
1196
|
+
* POSTs `/__sprigr/tool/on_actor_retired` to every install that declares a
|
|
1197
|
+
* tool of that name. The app should run its own self-disconnect
|
|
1198
|
+
* revoke-and-purge for that key and nothing else.
|
|
1199
|
+
*
|
|
1200
|
+
* // src/handlers/index.ts
|
|
1201
|
+
* import { actorRetiredHook } from '@sprigr/apps-app-sdk';
|
|
1202
|
+
* export default {
|
|
1203
|
+
* on_actor_retired: actorRetiredHook<MyEnv>(async (env, retired) => {
|
|
1204
|
+
* await revokeAndPurge(env, retired.actor, { cutoff: retired.requestedAt, ownerRef: retired.storageOwnerRef });
|
|
1205
|
+
* return { done: true };
|
|
1206
|
+
* }),
|
|
1207
|
+
* };
|
|
1208
|
+
*
|
|
1209
|
+
* Manifest: declare the tool with `"internal": true`. The platform keeps it
|
|
1210
|
+
* off every agent tool list and refuses it on the app-bridge invoke route
|
|
1211
|
+
* either way, because its body names whose grant to revoke.
|
|
1212
|
+
*
|
|
1213
|
+
* Contract:
|
|
1214
|
+
* - The dispatch carries NO actor. `actorRetiredHook` throws when `args.actor`
|
|
1215
|
+
* is present (a call that did not come from this hook), and when the body
|
|
1216
|
+
* is malformed. A throw reaches the platform as a 500, which it retries
|
|
1217
|
+
* and finally logs, so a contract mismatch is loud rather than a silent
|
|
1218
|
+
* "done".
|
|
1219
|
+
* - Return `{ done: true }` when finished, or when the rest is durably
|
|
1220
|
+
* queued by the app itself. Return `{ done: false, remaining }` to be
|
|
1221
|
+
* called again later (1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h; 8
|
|
1222
|
+
* deliveries in all). A thrown error is retried the same way.
|
|
1223
|
+
* - Idempotent: a second delivery for a key already purged must return
|
|
1224
|
+
* `{ done: true }`.
|
|
1225
|
+
* - `successorKey` is audit only. Never move the grant or the data to it.
|
|
1226
|
+
* - Delete only what existed by `requestedAt`.
|
|
1227
|
+
* - `storageOwnerRef` is the `owner_ref` a no-actor maintenance
|
|
1228
|
+
* `env.SPRIGR.files.list` reports for this person's per-user stored files;
|
|
1229
|
+
* pass it to a no-actor `env.SPRIGR.files.delete(key, { owner_ref })`.
|
|
1230
|
+
* Only the platform can derive it.
|
|
1231
|
+
*/
|
|
1232
|
+
|
|
1233
|
+
/** The manifest tool name the platform dispatches. */
|
|
1234
|
+
declare const ACTOR_RETIRED_HOOK_TOOL = "on_actor_retired";
|
|
1235
|
+
/** Why the key is being retired. */
|
|
1236
|
+
type ActorRetireReason = 'member_removed' | 'agent_deleted' | 'user_id_superseded' | 'admin_removed';
|
|
1237
|
+
declare const ACTOR_RETIRE_REASONS: readonly ActorRetireReason[];
|
|
1238
|
+
/** The wire body, exactly as the platform POSTs it (snake_case). */
|
|
1239
|
+
interface ActorRetiredHookBody {
|
|
1240
|
+
trigger: 'actor_retired';
|
|
1241
|
+
install_id: string;
|
|
1242
|
+
company_id: string;
|
|
1243
|
+
/** `u:<platform_user_id>` or `a:<agent_id>`. */
|
|
1244
|
+
actor_key: string;
|
|
1245
|
+
platform_user_id?: string;
|
|
1246
|
+
agent_id?: string;
|
|
1247
|
+
reason: ActorRetireReason;
|
|
1248
|
+
/** Audit only. */
|
|
1249
|
+
successor_key?: string;
|
|
1250
|
+
/** 32 hex chars: the per-user storage owner (`owner_ref`). */
|
|
1251
|
+
storage_owner_ref: string;
|
|
1252
|
+
requested_by?: string;
|
|
1253
|
+
/** ISO time; purge only what existed by then. */
|
|
1254
|
+
requested_at: string;
|
|
1255
|
+
/** 1 on the first delivery. */
|
|
1256
|
+
attempt: number;
|
|
1257
|
+
}
|
|
1258
|
+
/** The parsed body a handler receives. */
|
|
1259
|
+
interface ActorRetired {
|
|
1260
|
+
installId: string;
|
|
1261
|
+
companyId: string;
|
|
1262
|
+
/** The app-side key, equal to `actorKey(actor)`. */
|
|
1263
|
+
actorKey: string;
|
|
1264
|
+
/** The retired identity, shaped like a stamped actor so existing per-actor
|
|
1265
|
+
* helpers (token stores, walk keys) can be called with it. */
|
|
1266
|
+
actor: Actor;
|
|
1267
|
+
reason: ActorRetireReason;
|
|
1268
|
+
successorKey?: string;
|
|
1269
|
+
storageOwnerRef: string;
|
|
1270
|
+
requestedBy?: string;
|
|
1271
|
+
/** Epoch ms of `requested_at`. */
|
|
1272
|
+
requestedAt: number;
|
|
1273
|
+
attempt: number;
|
|
1274
|
+
}
|
|
1275
|
+
/** What the handler returns. */
|
|
1276
|
+
interface ActorRetiredResult {
|
|
1277
|
+
done: boolean;
|
|
1278
|
+
/** Anything that says what is left, for the platform's log. */
|
|
1279
|
+
remaining?: unknown;
|
|
1280
|
+
[extra: string]: unknown;
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Parse and check the hook body. Throws an Error naming the first problem;
|
|
1284
|
+
* never returns a partial value.
|
|
1285
|
+
*/
|
|
1286
|
+
declare function parseActorRetiredArgs(args: unknown): ActorRetired;
|
|
1287
|
+
/**
|
|
1288
|
+
* Wrap an `on_actor_retired` handler in the wrapper's `(args, env)` calling
|
|
1289
|
+
* convention. Unlike `tool()` it does NOT turn a throw into `{ ok: false }`:
|
|
1290
|
+
* the platform reads the returned `done`, and a swallowed error would read as
|
|
1291
|
+
* finished. A throw becomes a 500 the platform retries.
|
|
1292
|
+
*/
|
|
1293
|
+
declare function actorRetiredHook<Env>(fn: (env: Env, retired: ActorRetired) => Promise<ActorRetiredResult>): (args: unknown, env: Env) => Promise<ActorRetiredResult>;
|
|
1294
|
+
|
|
1188
1295
|
/**
|
|
1189
1296
|
* App-scoped R2 file storage (platform feature, 2026-06).
|
|
1190
1297
|
*
|
|
@@ -2365,6 +2472,79 @@ declare function connectDisconnect(env: WfpBridgeEnv, req: {
|
|
|
2365
2472
|
deleted: number;
|
|
2366
2473
|
}>;
|
|
2367
2474
|
|
|
2475
|
+
/**
|
|
2476
|
+
* Stored-file keys an agent can open (sprigr/sprigr-apps#2394).
|
|
2477
|
+
*
|
|
2478
|
+
* The platform hands an app its storage keys INSTALL-RELATIVE
|
|
2479
|
+
* (`drive/reads/<id>.docx`): that is the app's own view of its storage, and the
|
|
2480
|
+
* platform never names the install prefix or the per-user owner segment in a
|
|
2481
|
+
* files response. The platform tool read_file needs the ABSOLUTE key:
|
|
2482
|
+
*
|
|
2483
|
+
* `_apps/<installId>/<appKey>` (shared storage)
|
|
2484
|
+
* `_apps/<installId>/~u/<ownerHash>/<appKey>` (a manifest
|
|
2485
|
+
* actor_scoped_storage_prefixes key,
|
|
2486
|
+
* sprigr/sprigr-team#9621)
|
|
2487
|
+
*
|
|
2488
|
+
* A signed URL from env.SPRIGR.files.url carries exactly that key as its path,
|
|
2489
|
+
* so a tool that stores a file and mints a link can hand the agent a key that
|
|
2490
|
+
* opens. The github app found the same gap first (sprigr/sprigr-apps#2276,
|
|
2491
|
+
* deriveAgentFileKey); this version also accepts the owner segment.
|
|
2492
|
+
*/
|
|
2493
|
+
/** Root every marketplace-app file lives under. */
|
|
2494
|
+
declare const APP_FILES_ROOT = "_apps/";
|
|
2495
|
+
/** The platform's per-user owner segment (sprigr/sprigr-team#9621). An app never
|
|
2496
|
+
* names it in a key it hands env.SPRIGR.files; the platform refuses that. */
|
|
2497
|
+
declare const APP_OWNER_SEGMENT = "~u";
|
|
2498
|
+
/**
|
|
2499
|
+
* The absolute key behind a minted download URL, for the app key it was minted
|
|
2500
|
+
* for. Returns null, never a guess, unless the URL path is exactly
|
|
2501
|
+
* `_apps/<one segment>/<appKey>` or `_apps/<one segment>/~u/<one segment>/<appKey>`:
|
|
2502
|
+
* a key handed to an agent as openable that is not the stored object's key is
|
|
2503
|
+
* worse than no key.
|
|
2504
|
+
*/
|
|
2505
|
+
declare function agentFileKeyFromUrl(downloadUrl: string, appKey: string): string | null;
|
|
2506
|
+
/**
|
|
2507
|
+
* The key a tool returns as `file_key` for a file it stored under `appKey`
|
|
2508
|
+
* and minted `downloadUrl` for: the absolute key when it can be read off the
|
|
2509
|
+
* URL, otherwise the app key unchanged (the behaviour before sprigr/sprigr-apps#2394).
|
|
2510
|
+
*/
|
|
2511
|
+
declare function readableStoredKey(appKey: string, downloadUrl: string): string;
|
|
2512
|
+
/** A caller-supplied stored-file key, resolved to this install's app key. */
|
|
2513
|
+
type CallerKeyResolution = {
|
|
2514
|
+
ok: true;
|
|
2515
|
+
/** The install-relative key to hand env.SPRIGR.files. */
|
|
2516
|
+
appKey: string;
|
|
2517
|
+
/** True when the caller passed the absolute form or a signed link. */
|
|
2518
|
+
wasAbsolute: boolean;
|
|
2519
|
+
} | {
|
|
2520
|
+
ok: false;
|
|
2521
|
+
/** `other_install`: an absolute key for a different install. `malformed`:
|
|
2522
|
+
* an `_apps/` key or a link with no file path after the prefix. */
|
|
2523
|
+
reason: 'other_install' | 'malformed';
|
|
2524
|
+
/** The install the key names, for `other_install`. */
|
|
2525
|
+
installId?: string;
|
|
2526
|
+
};
|
|
2527
|
+
/**
|
|
2528
|
+
* Accept a stored-file key in any form a tool may have handed out and return
|
|
2529
|
+
* the app-relative key for env.SPRIGR.files:
|
|
2530
|
+
* - the app-relative key itself (`drive/reads/<id>.docx`): unchanged;
|
|
2531
|
+
* - the absolute key (`_apps/<installId>/[~u/<hash>/]drive/reads/<id>.docx`);
|
|
2532
|
+
* - the signed download link, whose path is that absolute key.
|
|
2533
|
+
*
|
|
2534
|
+
* The owner segment is dropped, not checked: the platform re-derives the
|
|
2535
|
+
* owner from the call's verified actor (sprigr/sprigr-team#9621), so another user's
|
|
2536
|
+
* key resolves to the caller's own copy, which usually does not exist. An
|
|
2537
|
+
* absolute key naming a different install is refused when `ownInstallId` is
|
|
2538
|
+
* known, because the platform would otherwise read THIS install's file at the
|
|
2539
|
+
* same relative path. When it is unknown the key is accepted: the platform
|
|
2540
|
+
* still confines every read to this install.
|
|
2541
|
+
*/
|
|
2542
|
+
declare function appKeyFromCallerKey(key: string, ownInstallId?: string | null): CallerKeyResolution;
|
|
2543
|
+
/** The sentence a refusal from appKeyFromCallerKey ends with, naming the key. */
|
|
2544
|
+
declare function callerKeyRefusal(key: string, r: Extract<CallerKeyResolution, {
|
|
2545
|
+
ok: false;
|
|
2546
|
+
}>): string;
|
|
2547
|
+
|
|
2368
2548
|
/**
|
|
2369
2549
|
* @sprigr/apps-app-sdk
|
|
2370
2550
|
*
|
|
@@ -2920,4 +3100,4 @@ interface ExecutionContextLike {
|
|
|
2920
3100
|
}
|
|
2921
3101
|
type HandlerFn<TArgs, TResult> = (args: TArgs, env: Record<string, unknown>, ctx?: ExecutionContextLike) => Promise<TResult>;
|
|
2922
3102
|
|
|
2923
|
-
export { APPROVAL_GRANTED_KEY, type Actor, 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 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, actorTool, appFileUrl, applyConfirmationPolicies, approvalHash, archiveOfferRefusal, base64ToBytes, buildConfirmationPolicy, buildMarketplaceWebhookUrl, buildPartialUpdateBody, bytesToBase64, 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, partialUpdateData, putAppFile, putAppFileStream, randomHex, recreatedWarning, refuseWithoutForce, requireApproval, resolveInstallBridge, resolveOAuthBouncerBase, resolveOAuthBouncerCallbackUrl, resolvePlatformWebhookBase, resolveViewerContext, resolveViewerUserId, runUndoApply, safeCapture, seq, serializeManifest, set, tool, undoEnvelope, validateLogEntries, validateLogEntry, verifyViewerToken, withSprigrEmitFallback, withSprigrInboxFallback, withSprigrLogFallback };
|
|
3103
|
+
export { ACTOR_RETIRED_HOOK_TOOL, ACTOR_RETIRE_REASONS, APPROVAL_GRANTED_KEY, 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, 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 };
|
package/dist/index.js
CHANGED
|
@@ -840,6 +840,74 @@ function createToolWrappers(opts = {}) {
|
|
|
840
840
|
};
|
|
841
841
|
}
|
|
842
842
|
|
|
843
|
+
// src/lifecycle.ts
|
|
844
|
+
var ACTOR_RETIRED_HOOK_TOOL = "on_actor_retired";
|
|
845
|
+
var ACTOR_RETIRE_REASONS = [
|
|
846
|
+
"member_removed",
|
|
847
|
+
"agent_deleted",
|
|
848
|
+
"user_id_superseded",
|
|
849
|
+
"admin_removed"
|
|
850
|
+
];
|
|
851
|
+
var OWNER_REF_RE = /^[0-9a-f]{32}$/;
|
|
852
|
+
var KEY_RE = /^([ua]):([A-Za-z0-9_\-.:@]{1,200})$/;
|
|
853
|
+
function parseActorRetiredArgs(args) {
|
|
854
|
+
if (!args || typeof args !== "object") throw new Error("on_actor_retired: body is not an object");
|
|
855
|
+
const b = args;
|
|
856
|
+
if (b.actor !== void 0 && b.actor !== null) {
|
|
857
|
+
throw new Error("on_actor_retired: refused, the call carries an actor; only the platform hook may call this");
|
|
858
|
+
}
|
|
859
|
+
if (b.trigger !== "actor_retired") throw new Error(`on_actor_retired: trigger must be actor_retired, got ${String(b.trigger)}`);
|
|
860
|
+
const str = (k) => {
|
|
861
|
+
const v = b[k];
|
|
862
|
+
if (typeof v !== "string" || v === "") throw new Error(`on_actor_retired: ${k} is required`);
|
|
863
|
+
return v;
|
|
864
|
+
};
|
|
865
|
+
const installId = str("install_id");
|
|
866
|
+
const companyId = str("company_id");
|
|
867
|
+
const key = str("actor_key");
|
|
868
|
+
const m = KEY_RE.exec(key);
|
|
869
|
+
if (!m) throw new Error(`on_actor_retired: actor_key must be u:<id> or a:<id>, got ${key.slice(0, 80)}`);
|
|
870
|
+
const actor = m[1] === "u" ? { platformUserId: m[2] } : { agentId: m[2] };
|
|
871
|
+
if (m[1] === "u" && b.platform_user_id !== void 0 && b.platform_user_id !== m[2]) {
|
|
872
|
+
throw new Error("on_actor_retired: platform_user_id does not match actor_key");
|
|
873
|
+
}
|
|
874
|
+
if (m[1] === "a" && b.agent_id !== void 0 && b.agent_id !== m[2]) {
|
|
875
|
+
throw new Error("on_actor_retired: agent_id does not match actor_key");
|
|
876
|
+
}
|
|
877
|
+
if (actorKey(actor) !== key) throw new Error("on_actor_retired: actor_key does not round-trip");
|
|
878
|
+
const reason = b.reason;
|
|
879
|
+
if (typeof reason !== "string" || !ACTOR_RETIRE_REASONS.includes(reason)) {
|
|
880
|
+
throw new Error(`on_actor_retired: unknown reason ${String(reason)}`);
|
|
881
|
+
}
|
|
882
|
+
const storageOwnerRef = str("storage_owner_ref");
|
|
883
|
+
if (!OWNER_REF_RE.test(storageOwnerRef)) throw new Error("on_actor_retired: storage_owner_ref must be 32 lowercase hex characters");
|
|
884
|
+
const requestedAt = Date.parse(str("requested_at"));
|
|
885
|
+
if (!Number.isFinite(requestedAt)) throw new Error("on_actor_retired: requested_at is not a date");
|
|
886
|
+
const attempt = typeof b.attempt === "number" && b.attempt >= 1 ? b.attempt : 1;
|
|
887
|
+
return {
|
|
888
|
+
installId,
|
|
889
|
+
companyId,
|
|
890
|
+
actorKey: key,
|
|
891
|
+
actor,
|
|
892
|
+
reason,
|
|
893
|
+
...typeof b.successor_key === "string" && b.successor_key ? { successorKey: b.successor_key } : {},
|
|
894
|
+
storageOwnerRef,
|
|
895
|
+
...typeof b.requested_by === "string" && b.requested_by ? { requestedBy: b.requested_by } : {},
|
|
896
|
+
requestedAt,
|
|
897
|
+
attempt
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
function actorRetiredHook(fn) {
|
|
901
|
+
return async (args, env) => {
|
|
902
|
+
const retired = parseActorRetiredArgs(args);
|
|
903
|
+
const result = await fn(env, retired);
|
|
904
|
+
if (!result || typeof result.done !== "boolean") {
|
|
905
|
+
throw new Error("on_actor_retired: the handler must return { done: boolean }");
|
|
906
|
+
}
|
|
907
|
+
return result;
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
|
|
843
911
|
// src/app-files.ts
|
|
844
912
|
var DEFAULT_PLATFORM_BASE = "https://webhooks.sprigr.com";
|
|
845
913
|
function platformBase(env) {
|
|
@@ -1616,8 +1684,88 @@ async function connectDisconnect(env, req) {
|
|
|
1616
1684
|
if (!bridge) throw new Error("connectDisconnect: SPRIGR_PLATFORM_BASE / SPRIGR_INSTALL_TOKEN are not bound");
|
|
1617
1685
|
return await installTokenPost(bridge, "/internal/wfp/connect/disconnect", req, { label: "connect disconnect" });
|
|
1618
1686
|
}
|
|
1687
|
+
|
|
1688
|
+
// src/stored-file-keys.ts
|
|
1689
|
+
var APP_FILES_ROOT = "_apps/";
|
|
1690
|
+
var APP_OWNER_SEGMENT = "~u";
|
|
1691
|
+
function isSegment(s) {
|
|
1692
|
+
return typeof s === "string" && s.length > 0 && !s.includes("/");
|
|
1693
|
+
}
|
|
1694
|
+
function trimLeadingSlashes(key) {
|
|
1695
|
+
return key.replace(/^\/+/, "");
|
|
1696
|
+
}
|
|
1697
|
+
function parseAbsolute(key) {
|
|
1698
|
+
if (!key.startsWith(APP_FILES_ROOT)) return null;
|
|
1699
|
+
const parts = key.slice(APP_FILES_ROOT.length).split("/");
|
|
1700
|
+
const installId = parts[0];
|
|
1701
|
+
if (!isSegment(installId)) return null;
|
|
1702
|
+
let i = 1;
|
|
1703
|
+
let ownerHash = null;
|
|
1704
|
+
if (parts[1] === APP_OWNER_SEGMENT) {
|
|
1705
|
+
if (!isSegment(parts[2])) return null;
|
|
1706
|
+
ownerHash = parts[2];
|
|
1707
|
+
i = 3;
|
|
1708
|
+
}
|
|
1709
|
+
const rest = parts.slice(i).join("/");
|
|
1710
|
+
if (!rest || rest.split("/").some((seg) => seg === "")) return null;
|
|
1711
|
+
return { installId, ownerHash, rest };
|
|
1712
|
+
}
|
|
1713
|
+
function urlPathKey(url) {
|
|
1714
|
+
try {
|
|
1715
|
+
return trimLeadingSlashes(decodeURIComponent(new URL(url).pathname));
|
|
1716
|
+
} catch {
|
|
1717
|
+
return null;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
function agentFileKeyFromUrl(downloadUrl, appKey) {
|
|
1721
|
+
const rel = trimLeadingSlashes(appKey);
|
|
1722
|
+
if (!rel) return null;
|
|
1723
|
+
const path = urlPathKey(downloadUrl);
|
|
1724
|
+
if (!path) return null;
|
|
1725
|
+
const parsed = parseAbsolute(path);
|
|
1726
|
+
if (!parsed || parsed.rest !== rel) return null;
|
|
1727
|
+
return path;
|
|
1728
|
+
}
|
|
1729
|
+
function readableStoredKey(appKey, downloadUrl) {
|
|
1730
|
+
return agentFileKeyFromUrl(downloadUrl, appKey) ?? appKey;
|
|
1731
|
+
}
|
|
1732
|
+
function appKeyFromCallerKey(key, ownInstallId) {
|
|
1733
|
+
let candidate = key.trim();
|
|
1734
|
+
let wasAbsolute = false;
|
|
1735
|
+
if (/^https?:\/\//i.test(candidate)) {
|
|
1736
|
+
const path = urlPathKey(candidate);
|
|
1737
|
+
if (!path || !path.startsWith(APP_FILES_ROOT)) return { ok: false, reason: "malformed" };
|
|
1738
|
+
candidate = path;
|
|
1739
|
+
}
|
|
1740
|
+
candidate = trimLeadingSlashes(candidate);
|
|
1741
|
+
if (candidate.startsWith(APP_FILES_ROOT)) {
|
|
1742
|
+
const parsed = parseAbsolute(candidate);
|
|
1743
|
+
if (!parsed) return { ok: false, reason: "malformed" };
|
|
1744
|
+
if (ownInstallId && parsed.installId !== ownInstallId) {
|
|
1745
|
+
return { ok: false, reason: "other_install", installId: parsed.installId };
|
|
1746
|
+
}
|
|
1747
|
+
return { ok: true, appKey: parsed.rest, wasAbsolute: true };
|
|
1748
|
+
}
|
|
1749
|
+
const parts = candidate.split("/");
|
|
1750
|
+
if (parts[0] === APP_OWNER_SEGMENT) {
|
|
1751
|
+
const rest = parts.slice(2).join("/");
|
|
1752
|
+
if (!isSegment(parts[1]) || !rest) return { ok: false, reason: "malformed" };
|
|
1753
|
+
return { ok: true, appKey: rest, wasAbsolute: true };
|
|
1754
|
+
}
|
|
1755
|
+
return { ok: true, appKey: candidate, wasAbsolute };
|
|
1756
|
+
}
|
|
1757
|
+
function callerKeyRefusal(key, r) {
|
|
1758
|
+
if (r.reason === "other_install") {
|
|
1759
|
+
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.`;
|
|
1760
|
+
}
|
|
1761
|
+
return `"${key}" is not a stored file key: it has no file path after the app storage prefix.`;
|
|
1762
|
+
}
|
|
1619
1763
|
export {
|
|
1764
|
+
ACTOR_RETIRED_HOOK_TOOL,
|
|
1765
|
+
ACTOR_RETIRE_REASONS,
|
|
1620
1766
|
APPROVAL_GRANTED_KEY,
|
|
1767
|
+
APP_FILES_ROOT,
|
|
1768
|
+
APP_OWNER_SEGMENT,
|
|
1621
1769
|
DATA_PARTIAL_UPDATE_PATH,
|
|
1622
1770
|
DEFAULT_EMIT_TIMEOUT_MS,
|
|
1623
1771
|
DEFAULT_LOG_TIMEOUT_MS,
|
|
@@ -1640,8 +1788,11 @@ export {
|
|
|
1640
1788
|
VIEWER_ROLE_HEADER,
|
|
1641
1789
|
VIEWER_USER_ID_HEADER,
|
|
1642
1790
|
actorKey,
|
|
1791
|
+
actorRetiredHook,
|
|
1643
1792
|
actorTool,
|
|
1793
|
+
agentFileKeyFromUrl,
|
|
1644
1794
|
appFileUrl,
|
|
1795
|
+
appKeyFromCallerKey,
|
|
1645
1796
|
applyConfirmationPolicies,
|
|
1646
1797
|
approvalHash,
|
|
1647
1798
|
archiveOfferRefusal,
|
|
@@ -1650,6 +1801,7 @@ export {
|
|
|
1650
1801
|
buildMarketplaceWebhookUrl,
|
|
1651
1802
|
buildPartialUpdateBody,
|
|
1652
1803
|
bytesToBase64,
|
|
1804
|
+
callerKeyRefusal,
|
|
1653
1805
|
canEmit,
|
|
1654
1806
|
canLog,
|
|
1655
1807
|
canPartialUpdate,
|
|
@@ -1684,10 +1836,12 @@ export {
|
|
|
1684
1836
|
overlaySprigr,
|
|
1685
1837
|
ownerFromActorKey,
|
|
1686
1838
|
parseActor,
|
|
1839
|
+
parseActorRetiredArgs,
|
|
1687
1840
|
partialUpdateData,
|
|
1688
1841
|
putAppFile,
|
|
1689
1842
|
putAppFileStream,
|
|
1690
1843
|
randomHex,
|
|
1844
|
+
readableStoredKey,
|
|
1691
1845
|
recreatedWarning,
|
|
1692
1846
|
refuseWithoutForce,
|
|
1693
1847
|
requireApproval,
|