@sprigr/apps-app-sdk 0.13.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
@@ -669,6 +669,12 @@ declare function withSprigrLogFallback<E extends WfpBridgeEnv & object>(env: E,
669
669
  * fails with `shard_field_invalid`: a patch cannot be routed to its shard
670
670
  * otherwise. The per-call cap is the same 1000 objects as `import`.
671
671
  *
672
+ * `withAcl: true` is a narrower mode: it re-stamps `acl_principals` on rows
673
+ * already in the app's `-acl-files` index and nothing else. Every patch must
674
+ * be exactly `{ objectID, acl_principals }`, and `createIfNotExists` and
675
+ * `index` cannot be combined with it. Content is written only by an ACL
676
+ * `import`, whole.
677
+ *
672
678
  * This module carries two things:
673
679
  *
674
680
  * 1. `buildPartialUpdateBody`, the wire body both transports send, with
@@ -687,10 +693,13 @@ declare function withSprigrLogFallback<E extends WfpBridgeEnv & object>(env: E,
687
693
  * swallow.
688
694
  *
689
695
  * Wire contract (workers/provisioning/src/wfp-data.ts in sprigr-team):
690
- * POST { index?, objects, createIfNotExists? }
696
+ * POST { index?, objects, createIfNotExists?, withAcl? }
691
697
  * 200 { ok: true, updated, skippedMissing, index, physical_indexes? }
692
698
  * 400 { error, detail, index? } with error one of too_many_objects,
693
- * invalid_object, unknown_data_index, shard_field_invalid, ...
699
+ * invalid_object, unknown_data_index, shard_field_invalid,
700
+ * acl_partial_update_fields, acl_partial_update_create_unsupported,
701
+ * acl_index_combination_unsupported, missing_acl_principals,
702
+ * invalid_acl_principal, ...
694
703
  * 401 / 404 bad token / inactive install
695
704
  */
696
705
 
@@ -708,6 +717,8 @@ interface SprigrDataPartialUpdateBody {
708
717
  index?: string;
709
718
  objects: SprigrDataPatch[];
710
719
  createIfNotExists: boolean;
720
+ /** Present (and true) only for an ACL re-stamp. */
721
+ withAcl?: true;
711
722
  }
712
723
  interface PartialUpdateDataOptions extends SprigrDataPartialUpdateOpts {
713
724
  /** Ceiling on the HTTP fallback. No default: a data write is awaited for its result. */
@@ -2085,6 +2096,268 @@ declare function deriveConfirmationPolicy(registry: Iterable<string>, opts: Deri
2085
2096
  */
2086
2097
  declare function commonDestructiveVerbs(subject: string): VerbRule[];
2087
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
+
2088
2361
  /**
2089
2362
  * @sprigr/apps-app-sdk
2090
2363
  *
@@ -2244,6 +2517,17 @@ interface SprigrDataPartialUpdateOpts extends SprigrDataIndexOpts {
2244
2517
  * missing target is counted in `skippedMissing` instead of being written.
2245
2518
  */
2246
2519
  createIfNotExists?: boolean;
2520
+ /**
2521
+ * Re-stamp `acl_principals` on rows already in the app's `-acl-files`
2522
+ * index without rewriting the rest of each row. Every object must be
2523
+ * exactly `{ objectID, acl_principals }` with a non-empty, valid principal
2524
+ * list; `createIfNotExists` and `index` are refused alongside it. Use it
2525
+ * when the source changes who may see an item without reporting the item
2526
+ * as changed (OneDrive's delta omits sharing-only changes). Needs a
2527
+ * platform with sprigr/sprigr-team#9843; older wrappers drop the flag,
2528
+ * which `partialUpdateData` detects and throws on.
2529
+ */
2530
+ withAcl?: boolean;
2247
2531
  }
2248
2532
  /** Reply of `env.SPRIGR.data.partialUpdate` (`POST /internal/wfp/data/partial-update`). */
2249
2533
  interface SprigrDataPartialUpdateResult {
@@ -2614,4 +2898,4 @@ interface ExecutionContextLike {
2614
2898
  }
2615
2899
  type HandlerFn<TArgs, TResult> = (args: TArgs, env: Record<string, unknown>, ctx?: ExecutionContextLike) => Promise<TResult>;
2616
2900
 
2617
- 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
@@ -512,6 +512,7 @@ function withSprigrLogFallback(env, opts) {
512
512
  // src/platform-data.ts
513
513
  var DATA_PARTIAL_UPDATE_PATH = "/internal/wfp/data/partial-update";
514
514
  var SPRIGR_DATA_MAX_OBJECTS_PER_CALL = 1e3;
515
+ var ACL_INDEX_SUFFIX = "-acl-files";
515
516
  var SprigrDataValidationError = class extends Error {
516
517
  error;
517
518
  index;
@@ -556,12 +557,51 @@ function buildPartialUpdateBody(objects, opts) {
556
557
  error: "unknown_data_index"
557
558
  });
558
559
  }
560
+ const withAcl = opts?.withAcl === true;
561
+ if (withAcl) checkAclRestamp(objects, opts, at);
559
562
  return {
560
563
  ...opts?.index !== void 0 ? { index: opts.index } : {},
561
564
  objects,
562
- createIfNotExists: opts?.createIfNotExists === true
565
+ createIfNotExists: opts?.createIfNotExists === true,
566
+ ...withAcl ? { withAcl: true } : {}
563
567
  };
564
568
  }
569
+ function checkAclRestamp(objects, opts, at) {
570
+ if (opts?.createIfNotExists === true) {
571
+ throw new SprigrDataValidationError(
572
+ `${at}: withAcl only re-stamps existing rows; createIfNotExists would create a row with principals and no content`,
573
+ { error: "acl_partial_update_create_unsupported" }
574
+ );
575
+ }
576
+ if (opts?.index !== void 0) {
577
+ throw new SprigrDataValidationError(`${at}: withAcl targets the reserved -acl-files index and cannot be combined with index`, {
578
+ error: "acl_index_combination_unsupported"
579
+ });
580
+ }
581
+ for (let i = 0; i < objects.length; i++) {
582
+ const o = objects[i];
583
+ const extra = Object.keys(o).find((k) => k !== "objectID" && k !== "acl_principals");
584
+ if (extra !== void 0) {
585
+ throw new SprigrDataValidationError(
586
+ `${at}: objects[${i}] carries "${extra}"; a withAcl partial update sets only objectID and acl_principals (write content with data.import withAcl)`,
587
+ { error: "acl_partial_update_fields", index: i }
588
+ );
589
+ }
590
+ const principals = o.acl_principals;
591
+ if (!Array.isArray(principals) || principals.length === 0) {
592
+ throw new SprigrDataValidationError(`${at}: objects[${i}] needs a non-empty acl_principals: string[]`, {
593
+ error: "missing_acl_principals",
594
+ index: i
595
+ });
596
+ }
597
+ }
598
+ }
599
+ function assertAclIndex(result) {
600
+ if (typeof result?.index === "string" && result.index.endsWith(ACL_INDEX_SUFFIX)) return result;
601
+ throw new Error(
602
+ `data.partialUpdate withAcl landed on ${String(result?.index)}, not the ${ACL_INDEX_SUFFIX} index: this app was built by a platform wrapper that drops withAcl. Republish the app to pick up the current wrapper.`
603
+ );
604
+ }
565
605
  function bindingPartialUpdate(env) {
566
606
  const data = env.SPRIGR?.data;
567
607
  return typeof data?.partialUpdate === "function" ? data.partialUpdate.bind(data) : null;
@@ -579,10 +619,12 @@ async function partialUpdateData(env, objects, opts) {
579
619
  const body = buildPartialUpdateBody(objects, dataOpts);
580
620
  const injected = bindingPartialUpdate(env);
581
621
  if (injected) {
582
- return injected(body.objects, {
622
+ const result = await injected(body.objects, {
583
623
  ...body.index !== void 0 ? { index: body.index } : {},
584
- createIfNotExists: body.createIfNotExists
624
+ createIfNotExists: body.createIfNotExists,
625
+ ...body.withAcl ? { withAcl: true } : {}
585
626
  });
627
+ return body.withAcl ? assertAclIndex(result) : result;
586
628
  }
587
629
  const bridge = resolveInstallBridge(env);
588
630
  if (!bridge) {
@@ -1446,6 +1488,109 @@ function commonDestructiveVerbs(subject) {
1446
1488
  { match: "deactivate_", tier: "always", describe: (_n, rest) => `Deactivate ${rest}${where}` }
1447
1489
  ];
1448
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
+ }
1449
1594
  export {
1450
1595
  APPROVAL_GRANTED_KEY,
1451
1596
  DATA_PARTIAL_UPDATE_PATH,
@@ -1454,6 +1599,8 @@ export {
1454
1599
  DEFAULT_MAX_FILE_BYTES,
1455
1600
  DEFAULT_MONEY_FIELDS,
1456
1601
  DEFAULT_WRITE_PREFIXES,
1602
+ INBOX_SEND_CONTRACT_V1,
1603
+ InboxSendRefusal,
1457
1604
  InvalidUndoEnvelopeError,
1458
1605
  NotConnectedError,
1459
1606
  SPRIGR_DATA_MAX_OBJECTS_PER_CALL,
@@ -1483,10 +1630,13 @@ export {
1483
1630
  canPartialUpdate,
1484
1631
  checkConfirmationPolicy,
1485
1632
  commonDestructiveVerbs,
1633
+ connectComplete,
1634
+ connectDisconnect,
1486
1635
  constantTimeEqual,
1487
1636
  createMarketplaceEmitter,
1488
1637
  createToolWrappers,
1489
1638
  decodeState,
1639
+ defineInboxSendHandler,
1490
1640
  deleteAppFile,
1491
1641
  deriveConfirmationPolicy,
1492
1642
  describeMissingBridge,
@@ -1533,5 +1683,6 @@ export {
1533
1683
  validateLogEntry,
1534
1684
  verifyViewerToken,
1535
1685
  withSprigrEmitFallback,
1686
+ withSprigrInboxFallback,
1536
1687
  withSprigrLogFallback
1537
1688
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprigr/apps-app-sdk",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",