@sprigr/apps-app-sdk 0.21.0 → 0.22.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
@@ -2778,6 +2778,136 @@ declare function callerKeyRefusal(key: string, r: Extract<CallerKeyResolution, {
2778
2778
  ok: false;
2779
2779
  }>): string;
2780
2780
 
2781
+ /**
2782
+ * Removing mirrored rows from an app's data index, from either execution
2783
+ * context (sprigr/sprigr-app-kit#123).
2784
+ *
2785
+ * An app that mirrors an account into its search index must drop that
2786
+ * account's rows when the install switches to another account or
2787
+ * disconnects; otherwise the next user searches the previous account's data
2788
+ * (sprigr/sprigr-apps#2900). Five apps each wrote the same pieces: a data
2789
+ * client that uses `env.SPRIGR.data` on a dispatched handler and the
2790
+ * install-token routes on an inline route, a prefix listing, and a chunked
2791
+ * delete that reports rather than throws. This module is the one copy.
2792
+ *
2793
+ * The pattern it serves, "snapshot, then purge exactly that":
2794
+ *
2795
+ * const before = await snapshotIndexIds(env, [{ prefix: 'trello:card:', index: 'cards' }]);
2796
+ * await storeNewAccountTokens(...); // the switch
2797
+ * const res = await deleteSnapshotIds(env, before);
2798
+ *
2799
+ * Rows written after the snapshot (the new account's first sync, a late
2800
+ * webhook) are not in it and survive. `purgeIndexByPrefix` is the one-step
2801
+ * form for a disconnect, where nothing new is being written.
2802
+ *
2803
+ * Neither purge function throws. A purge runs inside a connect or disconnect
2804
+ * flow that must finish; the result carries `error` and `truncated` so the
2805
+ * caller can audit an incomplete purge instead of losing it in a catch.
2806
+ *
2807
+ * Wire contract (workers/provisioning/src/wfp-data.ts in sprigr-team):
2808
+ * POST /internal/wfp/data/list-ids { prefix, index?, withAcl? }
2809
+ * 200 { objectIDs, total, truncated } (prefix must be non-empty; cap 50000)
2810
+ * POST /internal/wfp/data/delete { objectIDs, index?, withAcl? }
2811
+ * 200 { ok, deleted, index } (max 1000 ids; unknown ids are no-ops)
2812
+ */
2813
+
2814
+ /** Options for the id routes: a logical index, and the app's ACL index. */
2815
+ interface DataIdsOpts extends SprigrDataIndexOpts {
2816
+ withAcl?: boolean;
2817
+ }
2818
+ interface DataListIdsResult {
2819
+ objectIDs: string[];
2820
+ total: number;
2821
+ /** The platform returned its cap's worth and stopped: more rows exist. */
2822
+ truncated: boolean;
2823
+ }
2824
+ /** How a data call reaches the platform. `none` means it cannot. */
2825
+ type DataTransport = 'binding' | 'http' | 'none';
2826
+ /** The data surface with every member present, whichever transport carries it. */
2827
+ interface DataClient {
2828
+ via: Exclude<DataTransport, 'none'>;
2829
+ import(objects: Array<{
2830
+ objectID: string;
2831
+ [key: string]: unknown;
2832
+ }>, opts?: SprigrDataImportOpts): Promise<{
2833
+ ok: boolean;
2834
+ indexed: number;
2835
+ index: string;
2836
+ }>;
2837
+ search(opts?: SprigrDataSearchOpts): Promise<SprigrDataSearchResult>;
2838
+ get(objectID: string, opts?: SprigrDataIndexOpts): Promise<{
2839
+ ok: boolean;
2840
+ object: Record<string, unknown> | null;
2841
+ index: string;
2842
+ }>;
2843
+ partialUpdate(objects: Array<{
2844
+ objectID: string;
2845
+ [key: string]: unknown;
2846
+ }>, opts?: SprigrDataPartialUpdateOpts): Promise<SprigrDataPartialUpdateResult>;
2847
+ delete(objectIDs: string[], opts?: DataIdsOpts): Promise<{
2848
+ ok: boolean;
2849
+ deleted: number;
2850
+ index: string;
2851
+ }>;
2852
+ listIds(prefix: string, opts?: DataIdsOpts): Promise<DataListIdsResult>;
2853
+ }
2854
+ /** Max ids one delete call takes (the platform's per-call cap). */
2855
+ declare const DATA_DELETE_MAX_IDS = 1000;
2856
+ /**
2857
+ * The app's data surface on either transport, or null when neither exists.
2858
+ *
2859
+ * Uses `env.SPRIGR.data` when it carries BOTH `listIds` and `delete` (a
2860
+ * dispatched handler on a current wrapper build); otherwise the install-token
2861
+ * routes, which an inline Next route can reach and an older wrapper build
2862
+ * lacks members for. Every member then exists, so callers never feature-detect.
2863
+ */
2864
+ declare function makeDataClient(env: WfpBridgeEnv): DataClient | null;
2865
+ /** One prefix to list, in one index. A bare string is a prefix in the default index. */
2866
+ type IdPrefixSpec = string | ({
2867
+ prefix: string;
2868
+ } & DataIdsOpts);
2869
+ /** The ids an index held at one moment, grouped by the index they live in. */
2870
+ interface IdSnapshot {
2871
+ groups: Array<{
2872
+ index?: string;
2873
+ withAcl?: boolean;
2874
+ ids: string[];
2875
+ }>;
2876
+ /** Distinct ids across every group. */
2877
+ listed: number;
2878
+ /** A listing hit the platform's cap: the snapshot misses some rows. */
2879
+ truncated: boolean;
2880
+ via: DataTransport;
2881
+ /** Set when a listing failed; the groups hold whatever was listed before it. */
2882
+ error?: string;
2883
+ }
2884
+ interface PurgeResult {
2885
+ deleted: number;
2886
+ listed: number;
2887
+ truncated: boolean;
2888
+ via: DataTransport;
2889
+ /** Set when the purge stopped early. `deleted` says how far it got. */
2890
+ error?: string;
2891
+ }
2892
+ /**
2893
+ * Every id matching the prefixes right now. Never throws. With no data path
2894
+ * at all the app cannot have written rows, so the snapshot is empty with
2895
+ * `via: 'none'` and no error.
2896
+ */
2897
+ declare function snapshotIndexIds(env: WfpBridgeEnv, prefixes: readonly IdPrefixSpec[]): Promise<IdSnapshot>;
2898
+ /**
2899
+ * Delete exactly the ids in a snapshot, in chunks of `chunk` (default and
2900
+ * ceiling 1000). Never throws. A snapshot that carries a listing error is
2901
+ * still purged as far as it goes, and the error is passed on.
2902
+ */
2903
+ declare function deleteSnapshotIds(env: WfpBridgeEnv, snapshot: IdSnapshot, opts?: {
2904
+ chunk?: number;
2905
+ }): Promise<PurgeResult>;
2906
+ /** List, then delete, everything matching the prefixes. Never throws. */
2907
+ declare function purgeIndexByPrefix(env: WfpBridgeEnv, prefixes: readonly IdPrefixSpec[], opts?: {
2908
+ chunk?: number;
2909
+ }): Promise<PurgeResult>;
2910
+
2781
2911
  /**
2782
2912
  * @sprigr/apps-app-sdk
2783
2913
  *
@@ -3092,6 +3222,19 @@ interface SprigrDataApi {
3092
3222
  deleted: number;
3093
3223
  index: string;
3094
3224
  }>;
3225
+ /**
3226
+ * List the objectIDs that start with `prefix` (non-empty, so a listing is
3227
+ * always scoped). Returns at most 50000 with `truncated: true` past that.
3228
+ * `?`-optional for older wrapper builds; `makeDataClient` and
3229
+ * `purgeIndexByPrefix` fall back to the install-token route.
3230
+ */
3231
+ listIds?(prefix: string, opts?: SprigrDataIndexOpts & {
3232
+ withAcl?: boolean;
3233
+ }): Promise<{
3234
+ objectIDs: string[];
3235
+ total?: number;
3236
+ truncated: boolean;
3237
+ }>;
3095
3238
  }
3096
3239
  /**
3097
3240
  * Durable app file storage exposed on `env.SPRIGR.files` by the platform
@@ -3339,4 +3482,4 @@ interface ExecutionContextLike {
3339
3482
  }
3340
3483
  type HandlerFn<TArgs, TResult> = (args: TArgs, env: Record<string, unknown>, ctx?: ExecutionContextLike) => Promise<TResult>;
3341
3484
 
3342
- export { ACTOR_RETIRED_HOOK_TOOL, ACTOR_RETIRE_REASONS, APPROVAL_GRANTED_KEY, APPROVAL_PREVIEW_MAX, APP_FILES_ROOT, APP_OWNER_SEGMENT, type Actor, type ActorRetireReason, type ActorRetired, type ActorRetiredHookBody, type ActorRetiredResult, type AppApprovalEnvelope, type AppFileListItem, type AppFileUrlArgs, type AppFileUrlResult, type AppFilesEnv, type AppToolEffectsDeclaration, type AppUndoEnvelope, type ApplyPoliciesOptions, type ApprovalCoverage, type ApprovalGateOptions, type ApprovalSpec, type ArchiveOfferOptions, type ArchiveOfferRefusal, type CallerKeyResolution, type CaptureJournal, type ConfirmCondition, type ConfirmRule, type ConfirmationPolicy, type ConnectCompleteRequest, type ConnectCompleteResult, type D1Like, type D1PreparedStatementLike, DATA_PARTIAL_UPDATE_PATH, DEFAULT_EMIT_TIMEOUT_MS, DEFAULT_FILE_READ_TIMEOUT_MS, DEFAULT_LOG_TIMEOUT_MS, DEFAULT_MAX_FILE_BYTES, DEFAULT_MONEY_FIELDS, DEFAULT_WRITE_PREFIXES, type DerivePolicyOptions, type DerivedPolicy, type DispatcherApprovalGate, type DispatcherApprovalOptions, type EmitMarketplaceEventOptions, type EmitResult, type EmitTransport, type EventArgs, type ExecutionContextLike, FILE_REF_SOURCES_HELP, type FetchFileResult, type FetchWithRetryOptions, type FileRef, type FileRefContent, type FileRefEnv, FileRefError, type FileRefErrorCode, type FileRefStream, type FileRefVia, type GetAppFileResult, type HandlerFn, INBOX_SEND_CONTRACT_V1, type InboxAddress, type InboxAppendRequest, type InboxAppendResult, type InboxChannelDeclaration, type InboxFoldersRequest, type InboxFoldersResult, type InboxOwner, type InboxSendConnection, type InboxSendHandlerImpl, InboxSendRefusal, type InboxSendSupports, type InboxSendV1Args, type InboxSendV1Result, type InstallBridge, InvalidUndoEnvelopeError, type JournalRowLike, type LogToPlatformOptions, type ManifestDispatchBlock, type ManifestLike, type ManifestToolLike, type MarketplaceEventEmitOpts, type MarketplaceEventSourceIntegration, NotConnectedError, type OAuthBouncerSignals, type OAuthState, type OfferUndoArgs, type PartialUpdateDataOptions, type PlatformHostEnv, type PolicyCheckInput, type PolicySource, type PutAppFileArgs, type PutAppFileResult, type PutAppFileStreamArgs, type ReadFileRefOptions, type RequireApprovalOptions, type ResolveFileRefOptions, type ResolveViewerOptions, type ResolvedFileRef, type RestoreResult, type RestoreSpec, SPRIGR_DATA_MAX_OBJECTS_PER_CALL, SPRIGR_FILE_HOSTS, SPRIGR_LOG_LEVELS, SPRIGR_LOG_LIMITS, type ScheduleArgs, type SprigrDataAggregationCell, type SprigrDataAggregationResult, type SprigrDataAggregationSpec, type SprigrDataApi, type SprigrDataImportOpts, type SprigrDataIndexOpts, type SprigrDataPartialUpdateBody, type SprigrDataPartialUpdateOpts, type SprigrDataPartialUpdateResult, type SprigrDataPatch, type SprigrDataSearchOpts, type SprigrDataSearchResult, SprigrDataValidationError, type SprigrFilesApi, type SprigrFilesApiWithEdit, type SprigrFilesCreateInput, type SprigrFilesCreateResult, type SprigrFilesEditInput, type SprigrFilesEditResult, type SprigrFilesJobResult, type SprigrFilesListItem, type SprigrFilesListResult, type SprigrInboxApi, type SprigrLogEntry, type SprigrLogFn, type SprigrLogLevel, type SprigrLogResult, SprigrLogValidationError, type SyncedAttachment, type SyncedMessage, type ToolArgs, type ToolHandler, type ToolResult, type ToolWrapperOptions, UNIT_SEP, type UndoApplyOptions, type UndoApplyResult, type UndoCaptureSpec, type UndoFidelity, VIEWER_ANONYMOUS, VIEWER_CONTEXT_HEADER, VIEWER_PLATFORM_USER_ID_HEADER, VIEWER_ROLE_HEADER, VIEWER_USER_ID_HEADER, type VerbRule, type ViewerContext, type ViewerContextFailure, type ViewerContextVerification, type ViewerEnv, WORKSPACE_FILE_HOW_TO, type WebhookArgs, type WfpBridgeEnv, type WfpEmitReply, actorKey, actorRetiredHook, actorTool, agentFileKeyFromUrl, appFileUrl, appKeyFromCallerKey, applyConfirmationPolicies, approvalHash, approvalPreview, approvalRecipients, archiveOfferRefusal, attendedFindings, base64ToBytes, buildConfirmationPolicy, buildMarketplaceWebhookUrl, buildPartialUpdateBody, bytesToBase64, callerKeyRefusal, canEmit, canLog, canPartialUpdate, checkConfirmationPolicy, commonDestructiveVerbs, connectComplete, connectDisconnect, constantTimeEqual, createMarketplaceEmitter, createToolWrappers, decodeState, defineInboxSendHandler, deleteAppFile, deriveConfirmationPolicy, describeMissingBridge, dispatcherApproval, emitMarketplaceEvent, encodeState, fetchFileAsBase64, fetchFileBytes, fetchWithRetry, fullWarning, getAppFile, hmacSha256Hex, installTokenPost, isNotConnectedError, isStagingHost, listAppFiles, logToPlatform, manifestIsFresh, offerUndo, openFileRef, overlaySprigr, ownerFromActorKey, parseActor, parseActorRetiredArgs, partialUpdateData, putAppFile, putAppFileStream, randomHex, readFileRef, readableStoredKey, recreatedWarning, refuseWithoutForce, requireApproval, resolveFileRef, resolveInstallBridge, resolveOAuthBouncerBase, resolveOAuthBouncerCallbackUrl, resolvePlatformWebhookBase, resolveViewerContext, resolveViewerUserId, runUndoApply, safeCapture, seq, serializeManifest, set, tool, undoEnvelope, validateLogEntries, validateLogEntry, verifyViewerToken, withSprigrEmitFallback, withSprigrInboxFallback, withSprigrLogFallback };
3485
+ export { ACTOR_RETIRED_HOOK_TOOL, ACTOR_RETIRE_REASONS, APPROVAL_GRANTED_KEY, APPROVAL_PREVIEW_MAX, APP_FILES_ROOT, APP_OWNER_SEGMENT, type Actor, type ActorRetireReason, type ActorRetired, type ActorRetiredHookBody, type ActorRetiredResult, type AppApprovalEnvelope, type AppFileListItem, type AppFileUrlArgs, type AppFileUrlResult, type AppFilesEnv, type AppToolEffectsDeclaration, type AppUndoEnvelope, type ApplyPoliciesOptions, type ApprovalCoverage, type ApprovalGateOptions, type ApprovalSpec, type ArchiveOfferOptions, type ArchiveOfferRefusal, type CallerKeyResolution, type CaptureJournal, type ConfirmCondition, type ConfirmRule, type ConfirmationPolicy, type ConnectCompleteRequest, type ConnectCompleteResult, type D1Like, type D1PreparedStatementLike, DATA_DELETE_MAX_IDS, DATA_PARTIAL_UPDATE_PATH, DEFAULT_EMIT_TIMEOUT_MS, DEFAULT_FILE_READ_TIMEOUT_MS, DEFAULT_LOG_TIMEOUT_MS, DEFAULT_MAX_FILE_BYTES, DEFAULT_MONEY_FIELDS, DEFAULT_WRITE_PREFIXES, type DataClient, type DataIdsOpts, type DataListIdsResult, type DataTransport, type DerivePolicyOptions, type DerivedPolicy, type DispatcherApprovalGate, type DispatcherApprovalOptions, type EmitMarketplaceEventOptions, type EmitResult, type EmitTransport, type EventArgs, type ExecutionContextLike, FILE_REF_SOURCES_HELP, type FetchFileResult, type FetchWithRetryOptions, type FileRef, type FileRefContent, type FileRefEnv, FileRefError, type FileRefErrorCode, type FileRefStream, type FileRefVia, type GetAppFileResult, type HandlerFn, INBOX_SEND_CONTRACT_V1, type IdPrefixSpec, type IdSnapshot, 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 PurgeResult, type PutAppFileArgs, type PutAppFileResult, type PutAppFileStreamArgs, type ReadFileRefOptions, type RequireApprovalOptions, type ResolveFileRefOptions, type ResolveViewerOptions, type ResolvedFileRef, type RestoreResult, type RestoreSpec, SPRIGR_DATA_MAX_OBJECTS_PER_CALL, SPRIGR_FILE_HOSTS, SPRIGR_LOG_LEVELS, SPRIGR_LOG_LIMITS, type ScheduleArgs, type SprigrDataAggregationCell, type SprigrDataAggregationResult, type SprigrDataAggregationSpec, type SprigrDataApi, type SprigrDataImportOpts, type SprigrDataIndexOpts, type SprigrDataPartialUpdateBody, type SprigrDataPartialUpdateOpts, type SprigrDataPartialUpdateResult, type SprigrDataPatch, type SprigrDataSearchOpts, type SprigrDataSearchResult, SprigrDataValidationError, type SprigrFilesApi, type SprigrFilesApiWithEdit, type SprigrFilesCreateInput, type SprigrFilesCreateResult, type SprigrFilesEditInput, type SprigrFilesEditResult, type SprigrFilesJobResult, type SprigrFilesListItem, type SprigrFilesListResult, type SprigrInboxApi, type SprigrLogEntry, type SprigrLogFn, type SprigrLogLevel, type SprigrLogResult, SprigrLogValidationError, type SyncedAttachment, type SyncedMessage, type ToolArgs, type ToolHandler, type ToolResult, type ToolWrapperOptions, UNIT_SEP, type UndoApplyOptions, type UndoApplyResult, type UndoCaptureSpec, type UndoFidelity, VIEWER_ANONYMOUS, VIEWER_CONTEXT_HEADER, VIEWER_PLATFORM_USER_ID_HEADER, VIEWER_ROLE_HEADER, VIEWER_USER_ID_HEADER, type VerbRule, type ViewerContext, type ViewerContextFailure, type ViewerContextVerification, type ViewerEnv, WORKSPACE_FILE_HOW_TO, type WebhookArgs, type WfpBridgeEnv, type WfpEmitReply, actorKey, actorRetiredHook, actorTool, agentFileKeyFromUrl, appFileUrl, appKeyFromCallerKey, applyConfirmationPolicies, approvalHash, approvalPreview, approvalRecipients, archiveOfferRefusal, attendedFindings, base64ToBytes, buildConfirmationPolicy, buildMarketplaceWebhookUrl, buildPartialUpdateBody, bytesToBase64, callerKeyRefusal, canEmit, canLog, canPartialUpdate, checkConfirmationPolicy, commonDestructiveVerbs, connectComplete, connectDisconnect, constantTimeEqual, createMarketplaceEmitter, createToolWrappers, decodeState, defineInboxSendHandler, deleteAppFile, deleteSnapshotIds, deriveConfirmationPolicy, describeMissingBridge, dispatcherApproval, emitMarketplaceEvent, encodeState, fetchFileAsBase64, fetchFileBytes, fetchWithRetry, fullWarning, getAppFile, hmacSha256Hex, installTokenPost, isNotConnectedError, isStagingHost, listAppFiles, logToPlatform, makeDataClient, manifestIsFresh, offerUndo, openFileRef, overlaySprigr, ownerFromActorKey, parseActor, parseActorRetiredArgs, partialUpdateData, purgeIndexByPrefix, putAppFile, putAppFileStream, randomHex, readFileRef, readableStoredKey, recreatedWarning, refuseWithoutForce, requireApproval, resolveFileRef, resolveInstallBridge, resolveOAuthBouncerBase, resolveOAuthBouncerCallbackUrl, resolvePlatformWebhookBase, resolveViewerContext, resolveViewerUserId, runUndoApply, safeCapture, seq, serializeManifest, set, snapshotIndexIds, tool, undoEnvelope, validateLogEntries, validateLogEntry, verifyViewerToken, withSprigrEmitFallback, withSprigrInboxFallback, withSprigrLogFallback };
package/dist/index.js CHANGED
@@ -7,9 +7,9 @@ function constantTimeEqual(a, b) {
7
7
  }
8
8
  return mismatch === 0;
9
9
  }
10
- async function hmacSha256Hex(secret, message) {
10
+ async function hmacSha256Hex(secret, message2) {
11
11
  const keyData = new TextEncoder().encode(secret);
12
- const msgData = new TextEncoder().encode(message);
12
+ const msgData = new TextEncoder().encode(message2);
13
13
  const key = await crypto.subtle.importKey(
14
14
  "raw",
15
15
  keyData,
@@ -162,8 +162,8 @@ function platformBase(env) {
162
162
  }
163
163
  var AppFilesError = class extends Error {
164
164
  status;
165
- constructor(message, status) {
166
- super(message);
165
+ constructor(message2, status) {
166
+ super(message2);
167
167
  this.name = "AppFilesError";
168
168
  this.status = status;
169
169
  }
@@ -210,10 +210,10 @@ function putAppFile(env, args) {
210
210
  }
211
211
  var STREAM_PUT_MAX_ATTEMPTS = 3;
212
212
  var STREAM_PUT_BASE_DELAY_MS = 500;
213
- function isTransientPutStreamFailure(status, message) {
213
+ function isTransientPutStreamFailure(status, message2) {
214
214
  if (status === 503) return true;
215
215
  if (status !== 502) return false;
216
- const msg = message.toLowerCase();
216
+ const msg = message2.toLowerCase();
217
217
  return msg.includes("(10001)") || msg.includes("(10043)");
218
218
  }
219
219
  function sleep2(ms) {
@@ -351,8 +351,8 @@ var WORKSPACE_FILE_HOW_TO = "Apps cannot open a file in your own Sprigr storage
351
351
  var FILE_REF_SOURCES_HELP = "Pass exactly one of: file_key, a key this app's own tools returned; or file_url, an https Sprigr file link (on files.sprigr.com), either the download_url another app's tool returned or, for a file in your own Sprigr storage, a short-lived link from the files tool's generate_url.";
352
352
  var FileRefError = class extends Error {
353
353
  code;
354
- constructor(code, message) {
355
- super(message);
354
+ constructor(code, message2) {
355
+ super(message2);
356
356
  this.name = "FileRefError";
357
357
  this.code = code;
358
358
  }
@@ -444,19 +444,24 @@ async function openFileRef(env, ref, opts) {
444
444
  const failure = (err) => {
445
445
  if (err instanceof FileRefError) return err;
446
446
  const name = err instanceof Error ? err.name : "";
447
- const message = err instanceof Error ? err.message : String(err);
447
+ const message2 = err instanceof Error ? err.message : String(err);
448
448
  if (name === "TimeoutError" || name === "AbortError") {
449
449
  return new FileRefError("timeout", `Reading ${label} took longer than ${Math.round(timeoutMs / 1e3)}s, so it was not read.`);
450
450
  }
451
- return new FileRefError("read_failed", `Could not read ${label}: ${message}`);
451
+ return new FileRefError("read_failed", `Could not read ${label}: ${message2}`);
452
452
  };
453
453
  const openUrl = async (url, via, filename) => {
454
454
  let res;
455
455
  try {
456
- res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs), redirect: "error" });
456
+ res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs), redirect: "manual" });
457
457
  } catch (err) {
458
458
  throw failure(err);
459
459
  }
460
+ if (res.type === "opaqueredirect" || res.status >= 300 && res.status < 400) {
461
+ await res.body?.cancel().catch(() => {
462
+ });
463
+ throw new FileRefError("read_failed", `Could not read ${label}: the link redirected; refusing to follow.`);
464
+ }
460
465
  if (!res.ok) {
461
466
  await res.body?.cancel().catch(() => {
462
467
  });
@@ -739,8 +744,8 @@ var SprigrLogValidationError = class extends Error {
739
744
  field;
740
745
  max;
741
746
  length;
742
- constructor(message, fields) {
743
- super(message);
747
+ constructor(message2, fields) {
748
+ super(message2);
744
749
  this.name = "SprigrLogValidationError";
745
750
  this.index = fields.index;
746
751
  this.field = fields.field;
@@ -864,13 +869,13 @@ async function postLog(env, entries, timeoutMs) {
864
869
  return { ok: true, written: typeof reply.written === "number" ? reply.written : entries.length };
865
870
  } catch (err) {
866
871
  const e = err;
867
- const message = e instanceof Error ? e.message : String(err);
868
- console.warn(`[sprigr-log] log fallback failed: ${message}`);
872
+ const message2 = e instanceof Error ? e.message : String(err);
873
+ console.warn(`[sprigr-log] log fallback failed: ${message2}`);
869
874
  return {
870
875
  ok: false,
871
876
  error: e.error ?? (e.status !== void 0 ? "log_failed" : "log_transport_failed"),
872
877
  ...e.status !== void 0 ? { status: e.status } : {},
873
- detail: e.detail ?? message
878
+ detail: e.detail ?? message2
874
879
  };
875
880
  }
876
881
  }
@@ -919,8 +924,8 @@ var ACL_INDEX_SUFFIX = "-acl-files";
919
924
  var SprigrDataValidationError = class extends Error {
920
925
  error;
921
926
  index;
922
- constructor(message, fields) {
923
- super(message);
927
+ constructor(message2, fields) {
928
+ super(message2);
924
929
  this.name = "SprigrDataValidationError";
925
930
  this.error = fields.error;
926
931
  if (fields.index !== void 0) this.index = fields.index;
@@ -1093,7 +1098,7 @@ function base64urlDecode2(s) {
1093
1098
  return null;
1094
1099
  }
1095
1100
  }
1096
- async function hmacBytes(secret, message) {
1101
+ async function hmacBytes(secret, message2) {
1097
1102
  const key = await crypto.subtle.importKey(
1098
1103
  "raw",
1099
1104
  new TextEncoder().encode(secret),
@@ -1101,7 +1106,7 @@ async function hmacBytes(secret, message) {
1101
1106
  false,
1102
1107
  ["sign"]
1103
1108
  );
1104
- return new Uint8Array(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message)));
1109
+ return new Uint8Array(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message2)));
1105
1110
  }
1106
1111
  function constantTimeEqualBytes(a, b) {
1107
1112
  if (a.byteLength !== b.byteLength) return false;
@@ -1192,8 +1197,8 @@ async function resolveViewerUserId(headers, env, options = {}) {
1192
1197
 
1193
1198
  // src/tool-wrappers.ts
1194
1199
  var NotConnectedError = class extends Error {
1195
- constructor(message = "The calling actor has no connection with this app.") {
1196
- super(message);
1200
+ constructor(message2 = "The calling actor has no connection with this app.") {
1201
+ super(message2);
1197
1202
  this.name = "NotConnectedError";
1198
1203
  }
1199
1204
  };
@@ -1363,8 +1368,8 @@ function approvalPreview(value, max = APPROVAL_PREVIEW_MAX) {
1363
1368
 
1364
1369
  // src/write-protection/undo-envelope.ts
1365
1370
  var InvalidUndoEnvelopeError = class extends Error {
1366
- constructor(message) {
1367
- super(message);
1371
+ constructor(message2) {
1372
+ super(message2);
1368
1373
  this.name = "InvalidUndoEnvelopeError";
1369
1374
  }
1370
1375
  };
@@ -1964,8 +1969,8 @@ function withSprigrInboxFallback(env) {
1964
1969
  return overlaySprigr(env, { ...existing ?? {}, inbox: { append, folders } });
1965
1970
  }
1966
1971
  var InboxSendRefusal = class extends Error {
1967
- constructor(code, message) {
1968
- super(message);
1972
+ constructor(code, message2) {
1973
+ super(message2);
1969
1974
  this.code = code;
1970
1975
  this.name = "InboxSendRefusal";
1971
1976
  }
@@ -2041,6 +2046,137 @@ async function connectDisconnect(env, req) {
2041
2046
  if (!bridge) throw new Error("connectDisconnect: SPRIGR_PLATFORM_BASE / SPRIGR_INSTALL_TOKEN are not bound");
2042
2047
  return await installTokenPost(bridge, "/internal/wfp/connect/disconnect", req, { label: "connect disconnect" });
2043
2048
  }
2049
+
2050
+ // src/data-purge.ts
2051
+ var BRIDGE_TIMEOUT_MS = 2e4;
2052
+ var DATA_DELETE_MAX_IDS = 1e3;
2053
+ function injectedData(env) {
2054
+ const data = env.SPRIGR?.data;
2055
+ return data && typeof data === "object" ? data : null;
2056
+ }
2057
+ function idsOpts(opts) {
2058
+ return {
2059
+ ...opts?.index ? { index: opts.index } : {},
2060
+ ...opts?.withAcl ? { withAcl: true } : {}
2061
+ };
2062
+ }
2063
+ function makeDataClient(env) {
2064
+ const data = injectedData(env);
2065
+ if (data && typeof data.listIds === "function" && typeof data.delete === "function") {
2066
+ const d = data;
2067
+ return {
2068
+ via: "binding",
2069
+ import: (objects, opts) => d.import(objects, opts),
2070
+ search: (opts) => d.search(opts),
2071
+ get: (id, opts) => d.get(id, opts),
2072
+ partialUpdate: (objects, opts) => partialUpdateData(env, objects, opts),
2073
+ async delete(ids, opts) {
2074
+ const r = await d.delete(ids, opts);
2075
+ return { ok: r?.ok !== false, deleted: typeof r?.deleted === "number" ? r.deleted : ids.length, index: String(r?.index ?? opts?.index ?? "") };
2076
+ },
2077
+ async listIds(prefix, opts) {
2078
+ const r = await d.listIds(prefix, opts);
2079
+ const objectIDs = Array.isArray(r?.objectIDs) ? r.objectIDs : [];
2080
+ return { objectIDs, total: typeof r?.total === "number" ? r.total : objectIDs.length, truncated: r?.truncated === true };
2081
+ }
2082
+ };
2083
+ }
2084
+ const bridge = resolveInstallBridge(env);
2085
+ if (!bridge) return null;
2086
+ const post = (path, body, label) => installTokenPost(bridge, path, body, { label, timeoutMs: BRIDGE_TIMEOUT_MS });
2087
+ return {
2088
+ via: "http",
2089
+ async import(objects, opts) {
2090
+ const r = await post("/internal/wfp/data/import", { objects, ...opts ?? {} }, "data.import");
2091
+ return { ok: r.ok !== false, indexed: typeof r.indexed === "number" ? r.indexed : objects.length, index: String(r.index ?? opts?.index ?? "") };
2092
+ },
2093
+ async search(opts) {
2094
+ return await post("/internal/wfp/data/search", { ...opts ?? {} }, "data.search");
2095
+ },
2096
+ async get(objectID, opts) {
2097
+ const r = await post("/internal/wfp/data/get", { objectID, ...opts?.index ? { index: opts.index } : {} }, "data.get");
2098
+ return { ok: r.ok !== false, object: r.object ?? null, index: String(r.index ?? opts?.index ?? "") };
2099
+ },
2100
+ partialUpdate: (objects, opts) => partialUpdateData(env, objects, opts),
2101
+ async delete(objectIDs, opts) {
2102
+ const r = await post("/internal/wfp/data/delete", { objectIDs, ...idsOpts(opts) }, "data.delete");
2103
+ return { ok: r.ok !== false, deleted: typeof r.deleted === "number" ? r.deleted : objectIDs.length, index: String(r.index ?? opts?.index ?? "") };
2104
+ },
2105
+ async listIds(prefix, opts) {
2106
+ const r = await post("/internal/wfp/data/list-ids", { prefix, ...idsOpts(opts) }, "data.listIds");
2107
+ const objectIDs = Array.isArray(r.objectIDs) ? r.objectIDs : [];
2108
+ return { objectIDs, total: typeof r.total === "number" ? r.total : objectIDs.length, truncated: r.truncated === true };
2109
+ }
2110
+ };
2111
+ }
2112
+ var message = (err) => err instanceof Error ? err.message : String(err);
2113
+ function groupKey(index, withAcl) {
2114
+ return `${withAcl ? "acl" : "plain"}\0${index ?? ""}`;
2115
+ }
2116
+ async function snapshotIndexIds(env, prefixes) {
2117
+ const client = makeDataClient(env);
2118
+ const snap = { groups: [], listed: 0, truncated: false, via: client?.via ?? "none" };
2119
+ if (!client) return snap;
2120
+ const byGroup = /* @__PURE__ */ new Map();
2121
+ try {
2122
+ for (const spec of prefixes) {
2123
+ const s = typeof spec === "string" ? { prefix: spec } : spec;
2124
+ if (!s.prefix) throw new Error("every prefix must be non-empty (an empty one would list the whole index)");
2125
+ const key = groupKey(s.index, s.withAcl);
2126
+ let group = byGroup.get(key);
2127
+ if (!group) {
2128
+ group = { ...s.index ? { index: s.index } : {}, ...s.withAcl ? { withAcl: true } : {}, ids: /* @__PURE__ */ new Set() };
2129
+ byGroup.set(key, group);
2130
+ }
2131
+ const r = await client.listIds(s.prefix, { index: s.index, withAcl: s.withAcl });
2132
+ r.objectIDs.forEach((id) => {
2133
+ if (id.startsWith(s.prefix)) group.ids.add(id);
2134
+ });
2135
+ snap.truncated ||= r.truncated;
2136
+ }
2137
+ } catch (err) {
2138
+ snap.error = `list: ${message(err)}`;
2139
+ }
2140
+ for (const g of byGroup.values()) {
2141
+ snap.groups.push({ ...g.index ? { index: g.index } : {}, ...g.withAcl ? { withAcl: true } : {}, ids: [...g.ids] });
2142
+ snap.listed += g.ids.size;
2143
+ }
2144
+ return snap;
2145
+ }
2146
+ async function deleteSnapshotIds(env, snapshot, opts) {
2147
+ const res = {
2148
+ deleted: 0,
2149
+ listed: snapshot.listed,
2150
+ truncated: snapshot.truncated,
2151
+ via: snapshot.via,
2152
+ ...snapshot.error ? { error: snapshot.error } : {}
2153
+ };
2154
+ if (snapshot.listed === 0) return res;
2155
+ const client = makeDataClient(env);
2156
+ if (!client) {
2157
+ res.via = "none";
2158
+ res.error = [res.error, "delete: no data path (env.SPRIGR.data and the install token are both absent)"].filter(Boolean).join("; ");
2159
+ return res;
2160
+ }
2161
+ res.via = client.via;
2162
+ const chunk = Math.min(Math.max(Math.trunc(opts?.chunk ?? DATA_DELETE_MAX_IDS), 1), DATA_DELETE_MAX_IDS);
2163
+ try {
2164
+ for (const g of snapshot.groups) {
2165
+ for (let i = 0; i < g.ids.length; i += chunk) {
2166
+ const ids = g.ids.slice(i, i + chunk);
2167
+ const r = await client.delete(ids, { index: g.index, withAcl: g.withAcl });
2168
+ if (!r.ok) throw new Error(`the platform refused ${ids.length} id(s)${g.index ? ` in ${g.index}` : ""}`);
2169
+ res.deleted += r.deleted;
2170
+ }
2171
+ }
2172
+ } catch (err) {
2173
+ res.error = [res.error, `delete: ${message(err)}`].filter(Boolean).join("; ");
2174
+ }
2175
+ return res;
2176
+ }
2177
+ async function purgeIndexByPrefix(env, prefixes, opts) {
2178
+ return deleteSnapshotIds(env, await snapshotIndexIds(env, prefixes), opts);
2179
+ }
2044
2180
  export {
2045
2181
  ACTOR_RETIRED_HOOK_TOOL,
2046
2182
  ACTOR_RETIRE_REASONS,
@@ -2048,6 +2184,7 @@ export {
2048
2184
  APPROVAL_PREVIEW_MAX,
2049
2185
  APP_FILES_ROOT,
2050
2186
  APP_OWNER_SEGMENT,
2187
+ DATA_DELETE_MAX_IDS,
2051
2188
  DATA_PARTIAL_UPDATE_PATH,
2052
2189
  DEFAULT_EMIT_TIMEOUT_MS,
2053
2190
  DEFAULT_FILE_READ_TIMEOUT_MS,
@@ -2105,6 +2242,7 @@ export {
2105
2242
  decodeState,
2106
2243
  defineInboxSendHandler,
2107
2244
  deleteAppFile,
2245
+ deleteSnapshotIds,
2108
2246
  deriveConfirmationPolicy,
2109
2247
  describeMissingBridge,
2110
2248
  dispatcherApproval,
@@ -2121,6 +2259,7 @@ export {
2121
2259
  isStagingHost,
2122
2260
  listAppFiles,
2123
2261
  logToPlatform,
2262
+ makeDataClient,
2124
2263
  manifestIsFresh,
2125
2264
  offerUndo,
2126
2265
  openFileRef,
@@ -2129,6 +2268,7 @@ export {
2129
2268
  parseActor,
2130
2269
  parseActorRetiredArgs,
2131
2270
  partialUpdateData,
2271
+ purgeIndexByPrefix,
2132
2272
  putAppFile,
2133
2273
  putAppFileStream,
2134
2274
  randomHex,
@@ -2149,6 +2289,7 @@ export {
2149
2289
  seq,
2150
2290
  serializeManifest,
2151
2291
  set,
2292
+ snapshotIndexIds,
2152
2293
  tool,
2153
2294
  undoEnvelope,
2154
2295
  validateLogEntries,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprigr/apps-app-sdk",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",