@sprigr/apps-app-sdk 0.14.0 → 0.15.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/dist/index.d.ts CHANGED
@@ -2096,6 +2096,268 @@ declare function deriveConfirmationPolicy(registry: Iterable<string>, opts: Deri
2096
2096
  */
2097
2097
  declare function commonDestructiveVerbs(subject: string): VerbRule[];
2098
2098
 
2099
+ /**
2100
+ * The inbox contract for mail and ticket apps (sprigr-team decision 0150).
2101
+ *
2102
+ * An app that writes into the Sprigr inbox does four things, all typed here:
2103
+ *
2104
+ * 1. Declares each channel it writes under `capabilities.inbox_channels` in
2105
+ * its manifest (`InboxChannelDeclaration`). Append refuses undeclared
2106
+ * channels, and replies cannot send without a declared send tool.
2107
+ * 2. Appends synced messages with `env.SPRIGR.inbox.append` (`SyncedMessage`,
2108
+ * `InboxOwner` on every batch). `withSprigrInboxFallback` makes that work
2109
+ * from an inline route too.
2110
+ * 3. Implements the declared send tool on the one `sprigr.inbox.send/v1`
2111
+ * argument shape, through `defineInboxSendHandler`, which enforces the
2112
+ * mailbox-owner rule so it cannot be forgotten.
2113
+ * 4. If it collects a credential on its own page (no OAuth bouncer),
2114
+ * reports the connection with `connectComplete`, which forwards the
2115
+ * platform-signed viewer token so the platform knows who connected.
2116
+ */
2117
+
2118
+ declare const INBOX_SEND_CONTRACT_V1: "sprigr.inbox.send/v1";
2119
+ interface InboxSendSupports {
2120
+ bcc: boolean;
2121
+ html: boolean;
2122
+ recipients: boolean;
2123
+ importance: boolean;
2124
+ internal_note: boolean;
2125
+ }
2126
+ /** One entry of `capabilities.inbox_channels`. Validated at publish. */
2127
+ interface InboxChannelDeclaration {
2128
+ /** Must start with your slug in snake case plus `_` (`acme-mail` -> `acme_mail_...`). */
2129
+ channel: string;
2130
+ kind: 'email' | 'ticket';
2131
+ /** 1-40 printable characters, no `<` or `>`. */
2132
+ label: string;
2133
+ icon: 'mail' | 'ticket' | 'chat';
2134
+ send?: {
2135
+ tool: string;
2136
+ contract: typeof INBOX_SEND_CONTRACT_V1;
2137
+ returns_message_id: {
2138
+ send: boolean;
2139
+ reply: boolean;
2140
+ };
2141
+ attachments: 'file_key' | 'inline' | 'none';
2142
+ supports: InboxSendSupports;
2143
+ };
2144
+ signature_family?: string;
2145
+ attachment_repair_tool?: string;
2146
+ /** https only, host in `permissions.network_domains`; placeholders
2147
+ * `{source_thread_id}` and `{source_id}`. */
2148
+ deep_link_template?: string | null;
2149
+ }
2150
+ interface SyncedAttachment {
2151
+ filename: string;
2152
+ mimeType: string;
2153
+ size: number;
2154
+ /** Opaque to the platform; your `fetch_attachment` tool resolves it. It
2155
+ * must identify the connection, because that dispatch carries no actor. */
2156
+ providerAttachmentId?: string;
2157
+ contentId?: string;
2158
+ inline?: boolean;
2159
+ }
2160
+ interface SyncedMessage {
2161
+ /** Unique within the channel; the platform dedups on (channel, sourceId). */
2162
+ sourceId: string;
2163
+ sourceIndex: string;
2164
+ sourceThreadId?: string;
2165
+ /** RFC 5322 Message-ID (brackets optional). */
2166
+ rfcMessageId?: string;
2167
+ channel?: string;
2168
+ direction: 'inbound' | 'outbound' | 'internal';
2169
+ fromName?: string;
2170
+ fromEmail?: string;
2171
+ toRecipients?: Array<{
2172
+ name: string;
2173
+ email: string;
2174
+ }>;
2175
+ ccRecipients?: Array<{
2176
+ name: string;
2177
+ email: string;
2178
+ }>;
2179
+ subject?: string;
2180
+ /** Plain text. May be '' when `bodyHtml` carries the content. */
2181
+ body: string;
2182
+ bodyHtml?: string;
2183
+ /** ISO 8601. */
2184
+ timestamp: string;
2185
+ sourceIsRead?: boolean;
2186
+ tags?: string[];
2187
+ attachments?: SyncedAttachment[];
2188
+ }
2189
+ interface InboxAppendRequest {
2190
+ channel?: string;
2191
+ /** At most 500 per call. */
2192
+ messages: SyncedMessage[];
2193
+ /** The MAILBOX owner (from your connection row), never the dispatch caller. */
2194
+ owner?: InboxOwner;
2195
+ }
2196
+ interface InboxAppendResult {
2197
+ ok: boolean;
2198
+ threadsCreated: number;
2199
+ messagesLinked: number;
2200
+ threadIds: string[];
2201
+ contentlessSkipped?: number;
2202
+ }
2203
+ interface InboxFoldersRequest {
2204
+ channel?: string;
2205
+ /** At most 200 per call. Folder names match manual Sprigr folders. */
2206
+ items: Array<{
2207
+ sourceId: string;
2208
+ addFolders?: string[];
2209
+ removeFolders?: string[];
2210
+ }>;
2211
+ owner?: InboxOwner;
2212
+ }
2213
+ interface InboxFoldersResult {
2214
+ ok: boolean;
2215
+ filed: number;
2216
+ unfiled: number;
2217
+ skippedGuarded: number;
2218
+ unknownFolders: string[];
2219
+ }
2220
+ interface SprigrInboxApi {
2221
+ append(req: InboxAppendRequest): Promise<InboxAppendResult>;
2222
+ folders?(req: InboxFoldersRequest): Promise<InboxFoldersResult>;
2223
+ }
2224
+ /**
2225
+ * Return an env whose `SPRIGR.inbox` works from any execution context.
2226
+ *
2227
+ * `env.SPRIGR` is injected only on the `/__sprigr/*` dispatch path. An inline
2228
+ * route (a provider push receiver, your settings page) gets the raw bindings,
2229
+ * so this wires the same contract over the install-token HTTP bridge. Append
2230
+ * and folders THROW on a non-2xx, so a caller never advances a sync cursor
2231
+ * past messages the platform did not accept. With no bridge bindings the env
2232
+ * comes back unchanged and `SPRIGR.inbox` stays undefined: fail closed.
2233
+ */
2234
+ declare function withSprigrInboxFallback<E extends WfpBridgeEnv & object>(env: E): E;
2235
+ interface InboxAddress {
2236
+ email: string;
2237
+ name?: string;
2238
+ }
2239
+ /** What the platform sends your declared send tool. snake_case. */
2240
+ interface InboxSendV1Args {
2241
+ contract: typeof INBOX_SEND_CONTRACT_V1;
2242
+ from_email?: string;
2243
+ to?: InboxAddress[];
2244
+ cc?: InboxAddress[];
2245
+ bcc?: InboxAddress[];
2246
+ subject?: string;
2247
+ body_text: string;
2248
+ body_html?: string;
2249
+ attachments?: Array<{
2250
+ file_key: string;
2251
+ filename: string;
2252
+ content_type: string;
2253
+ }> | Array<{
2254
+ filename: string;
2255
+ mime_type?: string;
2256
+ base64_body: string;
2257
+ }>;
2258
+ /** `source_id` / `source_thread_id` are YOUR stored ids, present only when
2259
+ * your install received the mail being answered. */
2260
+ reply?: {
2261
+ source_id?: string;
2262
+ source_thread_id?: string;
2263
+ rfc_message_id?: string;
2264
+ references: string[];
2265
+ };
2266
+ internal_note?: boolean;
2267
+ author_email?: string;
2268
+ importance?: 'high' | 'low';
2269
+ idempotency_key?: string;
2270
+ }
2271
+ /** What your send tool returns. `message_id` must equal the `sourceId` your
2272
+ * sync will write for the sent copy, so the Sent echo dedups. */
2273
+ interface InboxSendV1Result {
2274
+ message_id?: string;
2275
+ internet_message_id?: string;
2276
+ thread_id?: string;
2277
+ }
2278
+ /** Your connection record, as far as the owner check needs it. */
2279
+ interface InboxSendConnection {
2280
+ /** `u:<platformUserId>` or `a:<agentId>`: the key the connection is filed under. */
2281
+ actorKey: string;
2282
+ }
2283
+ interface InboxSendHandlerImpl<E, C extends InboxSendConnection> {
2284
+ /** The connection that owns one of YOUR stored source ids, or null when the
2285
+ * id is not yours. Parse your own id format here. */
2286
+ connectionForSourceId(env: E, sourceId: string): Promise<C | null>;
2287
+ /** The connection this actor key sends a NEW mail from. When `fromEmail` is
2288
+ * set, return only a connection that may send as that address, else null. */
2289
+ connectionForActor(env: E, actorKey: string, fromEmail: string | undefined): Promise<C | null>;
2290
+ send(env: E, connection: C, args: InboxSendV1Args): Promise<InboxSendV1Result>;
2291
+ }
2292
+ declare class InboxSendRefusal extends Error {
2293
+ readonly code: 'no_caller_identity' | 'not_your_mailbox' | 'not_connected' | 'invalid_args';
2294
+ constructor(code: 'no_caller_identity' | 'not_your_mailbox' | 'not_connected' | 'invalid_args', message: string);
2295
+ }
2296
+ /**
2297
+ * Build the handler for your declared `sprigr.inbox.send/v1` tool.
2298
+ *
2299
+ * THE MAILBOX-OWNER RULE, enforced here so no app can omit it: replies are
2300
+ * dispatched under the WRITER's actor, and anyone who can read a thread can
2301
+ * press Reply. So the connection you send from must be filed under one of the
2302
+ * writer's own keys (`u:<platformUserId>`, `a:<agentId>`). A reply whose
2303
+ * source belongs to someone else's connection is REFUSED, never re-routed to
2304
+ * the writer's own mailbox and never sent from the owner's: either would
2305
+ * deliver mail the writer is not entitled to send.
2306
+ */
2307
+ declare function defineInboxSendHandler<E, C extends InboxSendConnection>(impl: InboxSendHandlerImpl<E, C>): (args: Record<string, unknown>, env: E) => Promise<ToolResult<InboxSendV1Result>>;
2308
+ interface ConnectCompleteRequest {
2309
+ /** The raw `X-Sprigr-Viewer` header of the PAGE request that submitted the
2310
+ * credential. The platform takes the user only from this. */
2311
+ viewer_token: string;
2312
+ bind?: {
2313
+ kind: 'user';
2314
+ } | {
2315
+ kind: 'agent';
2316
+ agent_id: string;
2317
+ };
2318
+ account: {
2319
+ email: string;
2320
+ name?: string;
2321
+ };
2322
+ identities?: Array<{
2323
+ email: string;
2324
+ name?: string;
2325
+ }>;
2326
+ }
2327
+ interface ConnectCompleteResult {
2328
+ ok: boolean;
2329
+ /** File your connection under this key. */
2330
+ actor_key: string;
2331
+ identities: {
2332
+ inserted: number;
2333
+ updated: number;
2334
+ deleted: number;
2335
+ };
2336
+ /** The same address already connected through another app in the company. */
2337
+ existing_mailboxes: Array<{
2338
+ app_slug: string;
2339
+ email: string;
2340
+ account_email: string;
2341
+ }>;
2342
+ }
2343
+ /**
2344
+ * Report a mailbox connected on your own page (token paste, app password).
2345
+ * Call it from the inline route that handled the page's POST, passing that
2346
+ * request's `X-Sprigr-Viewer` header. Throws on a non-2xx with `.status` and
2347
+ * `.error` (`viewer_unverified`, `viewer_not_a_member`, `agent_bind_refused`,
2348
+ * `agent_bind_requires_admin`, ...).
2349
+ */
2350
+ declare function connectComplete(env: WfpBridgeEnv, req: ConnectCompleteRequest): Promise<ConnectCompleteResult>;
2351
+ /** Retire a connection's From-picker identities (install token only, so a
2352
+ * background sync can call it when a credential dies). */
2353
+ declare function connectDisconnect(env: WfpBridgeEnv, req: {
2354
+ actor_key: string;
2355
+ account_email?: string;
2356
+ }): Promise<{
2357
+ ok: boolean;
2358
+ deleted: number;
2359
+ }>;
2360
+
2099
2361
  /**
2100
2362
  * @sprigr/apps-app-sdk
2101
2363
  *
@@ -2636,4 +2898,4 @@ interface ExecutionContextLike {
2636
2898
  }
2637
2899
  type HandlerFn<TArgs, TResult> = (args: TArgs, env: Record<string, unknown>, ctx?: ExecutionContextLike) => Promise<TResult>;
2638
2900
 
2639
- 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 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, type InboxOwner, 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 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 SprigrLogEntry, type SprigrLogFn, type SprigrLogLevel, type SprigrLogResult, SprigrLogValidationError, 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, constantTimeEqual, createMarketplaceEmitter, createToolWrappers, decodeState, 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, withSprigrLogFallback };
2901
+ 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 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 };
package/dist/index.js CHANGED
@@ -1488,6 +1488,109 @@ function commonDestructiveVerbs(subject) {
1488
1488
  { match: "deactivate_", tier: "always", describe: (_n, rest) => `Deactivate ${rest}${where}` }
1489
1489
  ];
1490
1490
  }
1491
+
1492
+ // src/inbox.ts
1493
+ var INBOX_SEND_CONTRACT_V1 = "sprigr.inbox.send/v1";
1494
+ function withSprigrInboxFallback(env) {
1495
+ const bridge = resolveInstallBridge(env);
1496
+ const existing = env.SPRIGR ?? void 0;
1497
+ const folders = bridge ? async (req) => await installTokenPost(bridge, "/internal/wfp/inbox/folders", req, { label: "inbox folders" }) : void 0;
1498
+ if (existing?.inbox) {
1499
+ if (existing.inbox.folders || !folders) return env;
1500
+ const inbox = existing.inbox;
1501
+ return overlaySprigr(env, { ...existing, inbox: { append: inbox.append.bind(inbox), folders } });
1502
+ }
1503
+ if (!bridge) return env;
1504
+ const append = async (req) => {
1505
+ const r = await installTokenPost(bridge, "/internal/wfp/inbox/append", req, { label: "inbox append" });
1506
+ return {
1507
+ ok: r.ok !== false,
1508
+ threadsCreated: r.threadsCreated ?? 0,
1509
+ messagesLinked: r.messagesLinked ?? 0,
1510
+ threadIds: Array.isArray(r.threadIds) ? r.threadIds : [],
1511
+ ...typeof r.contentlessSkipped === "number" ? { contentlessSkipped: r.contentlessSkipped } : {}
1512
+ };
1513
+ };
1514
+ return overlaySprigr(env, { ...existing ?? {}, inbox: { append, folders } });
1515
+ }
1516
+ var InboxSendRefusal = class extends Error {
1517
+ constructor(code, message) {
1518
+ super(message);
1519
+ this.code = code;
1520
+ this.name = "InboxSendRefusal";
1521
+ }
1522
+ code;
1523
+ };
1524
+ function defineInboxSendHandler(impl) {
1525
+ return async (rawArgs, env) => {
1526
+ try {
1527
+ const actor = parseActor(rawArgs);
1528
+ const keys = [
1529
+ ...actor?.platformUserId ? [`u:${actor.platformUserId}`] : [],
1530
+ ...actor?.agentId ? [`a:${actor.agentId}`] : []
1531
+ ];
1532
+ if (keys.length === 0) {
1533
+ throw new InboxSendRefusal("no_caller_identity", "The platform stamped no sender identity on this send.");
1534
+ }
1535
+ const args = rawArgs;
1536
+ if (args.contract !== INBOX_SEND_CONTRACT_V1 || typeof args.body_text !== "string") {
1537
+ throw new InboxSendRefusal("invalid_args", `Expected ${INBOX_SEND_CONTRACT_V1} arguments.`);
1538
+ }
1539
+ let connection = null;
1540
+ const sourceId = args.reply?.source_id;
1541
+ if (sourceId) {
1542
+ const owner = await impl.connectionForSourceId(env, sourceId);
1543
+ if (owner) {
1544
+ if (!keys.includes(owner.actorKey)) {
1545
+ throw new InboxSendRefusal(
1546
+ "not_your_mailbox",
1547
+ "This conversation arrived in a mailbox that belongs to someone else, so you cannot reply from it."
1548
+ );
1549
+ }
1550
+ if (args.from_email) {
1551
+ const asFrom = await impl.connectionForActor(env, owner.actorKey, args.from_email);
1552
+ if (!asFrom || asFrom.actorKey !== owner.actorKey) {
1553
+ throw new InboxSendRefusal("not_your_mailbox", `This mailbox cannot send as ${args.from_email}.`);
1554
+ }
1555
+ }
1556
+ connection = owner;
1557
+ }
1558
+ }
1559
+ if (!connection) {
1560
+ for (const key of keys) {
1561
+ connection = await impl.connectionForActor(env, key, args.from_email);
1562
+ if (connection) break;
1563
+ }
1564
+ }
1565
+ if (!connection) {
1566
+ throw new InboxSendRefusal(
1567
+ "not_connected",
1568
+ args.from_email ? `You have no connection that can send as ${args.from_email}.` : "You have not connected a mailbox with this app."
1569
+ );
1570
+ }
1571
+ if (!keys.includes(connection.actorKey)) {
1572
+ throw new InboxSendRefusal("not_your_mailbox", "Resolved connection does not belong to the sender.");
1573
+ }
1574
+ return { ok: true, result: await impl.send(env, connection, args) };
1575
+ } catch (err) {
1576
+ if (err instanceof InboxSendRefusal || err instanceof Error && err.name === "InboxSendRefusal") {
1577
+ const code = err.code;
1578
+ return { ok: false, error: `${code}: ${err.message}`, status: code === "invalid_args" ? 400 : 412 };
1579
+ }
1580
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
1581
+ }
1582
+ };
1583
+ }
1584
+ async function connectComplete(env, req) {
1585
+ const bridge = resolveInstallBridge(env);
1586
+ if (!bridge) throw new Error("connectComplete: SPRIGR_PLATFORM_BASE / SPRIGR_INSTALL_TOKEN are not bound");
1587
+ return await installTokenPost(bridge, "/internal/wfp/connect/complete", req, { label: "connect complete" });
1588
+ }
1589
+ async function connectDisconnect(env, req) {
1590
+ const bridge = resolveInstallBridge(env);
1591
+ if (!bridge) throw new Error("connectDisconnect: SPRIGR_PLATFORM_BASE / SPRIGR_INSTALL_TOKEN are not bound");
1592
+ return await installTokenPost(bridge, "/internal/wfp/connect/disconnect", req, { label: "connect disconnect" });
1593
+ }
1491
1594
  export {
1492
1595
  APPROVAL_GRANTED_KEY,
1493
1596
  DATA_PARTIAL_UPDATE_PATH,
@@ -1496,6 +1599,8 @@ export {
1496
1599
  DEFAULT_MAX_FILE_BYTES,
1497
1600
  DEFAULT_MONEY_FIELDS,
1498
1601
  DEFAULT_WRITE_PREFIXES,
1602
+ INBOX_SEND_CONTRACT_V1,
1603
+ InboxSendRefusal,
1499
1604
  InvalidUndoEnvelopeError,
1500
1605
  NotConnectedError,
1501
1606
  SPRIGR_DATA_MAX_OBJECTS_PER_CALL,
@@ -1525,10 +1630,13 @@ export {
1525
1630
  canPartialUpdate,
1526
1631
  checkConfirmationPolicy,
1527
1632
  commonDestructiveVerbs,
1633
+ connectComplete,
1634
+ connectDisconnect,
1528
1635
  constantTimeEqual,
1529
1636
  createMarketplaceEmitter,
1530
1637
  createToolWrappers,
1531
1638
  decodeState,
1639
+ defineInboxSendHandler,
1532
1640
  deleteAppFile,
1533
1641
  deriveConfirmationPolicy,
1534
1642
  describeMissingBridge,
@@ -1575,5 +1683,6 @@ export {
1575
1683
  validateLogEntry,
1576
1684
  verifyViewerToken,
1577
1685
  withSprigrEmitFallback,
1686
+ withSprigrInboxFallback,
1578
1687
  withSprigrLogFallback
1579
1688
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprigr/apps-app-sdk",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",