@sprigr/apps-app-sdk 0.8.7 → 0.9.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
@@ -582,6 +582,168 @@ interface InboxOwner {
582
582
  */
583
583
  declare function ownerFromActorKey(key: string | null | undefined): InboxOwner | null;
584
584
 
585
+ /**
586
+ * Who is looking at this app's UI right now.
587
+ *
588
+ * A marketplace app UI is embedded in the portal iframe with only
589
+ * `#install_id=...` in its src, so nothing about the human viewer arrives in
590
+ * the URL. It arrives in HTTP headers the platform's `website-serve` worker
591
+ * stamps on every dispatch, and this module is the ONE place that reads them.
592
+ *
593
+ * ## Use this, do not hand-roll `src/lib/viewer.ts`
594
+ *
595
+ * Twelve apps in `sprigr-apps` each carried their own copy of this logic as
596
+ * of 2026-09-03, all textually different. Five apps had a query-param
597
+ * fallback that outranked the header, which is a connection-hijack bug (any
598
+ * tenant member could bind their own provider account to a colleague's actor
599
+ * row): `sprigr-apps` #1487-#1491, and `sprigr-apps#474` before them. That is
600
+ * why this lives in the SDK.
601
+ *
602
+ * **Never take a viewer identity from a query param, a form field, a cookie
603
+ * your app set, or a `postMessage` payload — not even as a fallback for when
604
+ * the header is missing.** A missing header means "no viewer"; it does not
605
+ * mean "believe the client".
606
+ *
607
+ * ## Two channels, one preferred
608
+ *
609
+ * - **`X-Sprigr-Viewer`** (preferred, platform issue #3210): a signed token
610
+ * naming the viewer, bound to YOUR install and expiring in ~2 minutes.
611
+ * Everything in it is inside an HMAC your install alone can check, using
612
+ * the `SPRIGR_VIEWER_SECRET` binding the platform derives per install. It
613
+ * is the only channel that carries a display name.
614
+ * - **`x-sprigr-platform-user-id`** (legacy): the viewer's OIDC sub as a
615
+ * plain header. Not signed. It rests on transport integrity: your Worker
616
+ * is reachable only through the platform's WFP dispatch, and the platform
617
+ * overwrites the header unconditionally on every forward, so a browser
618
+ * cannot smuggle its own value through. That is a real guarantee, but you
619
+ * cannot verify it yourself.
620
+ *
621
+ * `resolveViewerContext` prefers the signed channel and returns
622
+ * `trust: 'verified'`. The legacy channel is used ONLY when you pass
623
+ * `allowTransportFallback: true`, and then reports `trust: 'transport'`. The
624
+ * fallback is opt-in per call site on purpose: it is needed while an install
625
+ * predates its `SPRIGR_VIEWER_SECRET` binding (the platform binds it on the
626
+ * install's next build or rebind), and it should be dropped once every
627
+ * install has been rebound.
628
+ */
629
+ /** Header carrying the signed viewer context. */
630
+ declare const VIEWER_CONTEXT_HEADER = "x-sprigr-viewer";
631
+ /** Legacy transport-attested header carrying the viewer's OIDC sub. */
632
+ declare const VIEWER_PLATFORM_USER_ID_HEADER = "x-sprigr-platform-user-id";
633
+ /** Legacy transport-attested header carrying the sprigr-team `users.id`. */
634
+ declare const VIEWER_USER_ID_HEADER = "x-sprigr-user-id";
635
+ /** Legacy transport-attested header carrying the viewer's company role. */
636
+ declare const VIEWER_ROLE_HEADER = "x-sprigr-role";
637
+ /** Sentinel both id headers carry when nobody is signed in. */
638
+ declare const VIEWER_ANONYMOUS = "anonymous";
639
+ /** Raw wire claims. snake_case because they are the signed payload. */
640
+ interface WireClaims {
641
+ install_id: string;
642
+ company_id: string;
643
+ user_id: string;
644
+ platform_user_id: string;
645
+ role: string;
646
+ display_name?: string;
647
+ email?: string;
648
+ issued_at: number;
649
+ expires_at: number;
650
+ }
651
+ /** The resolved viewer. */
652
+ interface ViewerContext {
653
+ /**
654
+ * How this was established.
655
+ *
656
+ * - `'verified'` — the signed `X-Sprigr-Viewer` token checked out against
657
+ * your install's own key. Authorise on this.
658
+ * - `'transport'` — the legacy unsigned header, accepted because the call
659
+ * site passed `allowTransportFallback`. Same practical guarantee the
660
+ * platform has always given, but nothing was cryptographically checked
661
+ * and `displayName` / `email` are never available.
662
+ */
663
+ trust: 'verified' | 'transport';
664
+ /** OIDC sub (`usr_...`) of the viewer, or null when signed out. This is
665
+ * what per-actor state keys on. */
666
+ platformUserId: string | null;
667
+ /** sprigr-team `users.id` row id, or null when signed out. */
668
+ userId: string | null;
669
+ /** Company that owns the install. Null on the legacy channel when the
670
+ * header is absent. */
671
+ companyId: string | null;
672
+ /** The viewer's role at the company, or null when signed out. */
673
+ role: string | null;
674
+ /** Display name. Only ever present on a verified context. DISPLAY ONLY —
675
+ * never branch authorisation on it. */
676
+ displayName: string | null;
677
+ /** Email. Only ever present on a verified context. DISPLAY ONLY. */
678
+ email: string | null;
679
+ /** The install the context was minted for. Null on the legacy channel. */
680
+ installId: string | null;
681
+ }
682
+ interface ViewerEnv {
683
+ /** Per-install key the platform derives and binds. Absent on an install
684
+ * that has not been rebuilt since the platform started binding it. */
685
+ SPRIGR_VIEWER_SECRET?: string;
686
+ /** This install's id, bound by the platform. Used to refuse a context
687
+ * minted for a different install. */
688
+ INSTALL_ID?: string;
689
+ }
690
+ interface ResolveViewerOptions {
691
+ /**
692
+ * Accept the legacy unsigned `x-sprigr-platform-user-id` header when no
693
+ * verified token is available. Defaults to FALSE: a call site that wants
694
+ * the weaker guarantee has to say so, so the weaker path is visible in
695
+ * review instead of being the silent default.
696
+ */
697
+ allowTransportFallback?: boolean;
698
+ /** Override the clock, for tests. Epoch seconds. */
699
+ nowSeconds?: number;
700
+ /** Override the future-issued tolerance, in seconds. */
701
+ clockSkewSeconds?: number;
702
+ }
703
+ type ViewerContextFailure = 'missing' | 'no_secret' | 'malformed' | 'bad_signature' | 'expired' | 'not_yet_valid' | 'install_mismatch';
704
+ type ViewerContextVerification = {
705
+ ok: true;
706
+ claims: WireClaims;
707
+ } | {
708
+ ok: false;
709
+ reason: ViewerContextFailure;
710
+ };
711
+ /**
712
+ * Verify a raw `X-Sprigr-Viewer` token.
713
+ *
714
+ * Most apps want `resolveViewerContext` instead; reach for this when you
715
+ * need to log or branch on the specific failure reason. Do NOT surface the
716
+ * reason to the browser: collapse every failure into the same "no viewer"
717
+ * outcome so a caller learns nothing from probing.
718
+ *
719
+ * The signature is checked before any claim, so nothing about an unsigned
720
+ * payload is observable through the failure reason.
721
+ */
722
+ declare function verifyViewerToken(token: string | null | undefined, env: ViewerEnv, options?: ResolveViewerOptions): Promise<ViewerContextVerification>;
723
+ /**
724
+ * Resolve the viewer from a request's headers.
725
+ *
726
+ * const viewer = await resolveViewerContext(request.headers, env);
727
+ * if (!viewer?.platformUserId) return jsonError(401, 'sign in to continue');
728
+ * // authorise on viewer.platformUserId, render viewer.displayName
729
+ *
730
+ * Returns null when there is no viewer at all: no token (or one that did not
731
+ * verify) and either no legacy header or no `allowTransportFallback`. A
732
+ * signed-out visitor on a verified context comes back non-null with every id
733
+ * field null, so an app can tell "nobody is signed in" from "I cannot tell".
734
+ */
735
+ declare function resolveViewerContext(headers: Headers, env: ViewerEnv, options?: ResolveViewerOptions): Promise<ViewerContext | null>;
736
+ /**
737
+ * The viewer's platform user id, or null.
738
+ *
739
+ * The narrow shape the twelve hand-rolled `viewer.ts` copies exposed, so an
740
+ * app can migrate onto the SDK in one import change and adopt the richer
741
+ * context later. `allowTransportFallback` defaults to TRUE here and only
742
+ * here, because that is exactly what those copies did — an app that wants
743
+ * the stronger guarantee should call `resolveViewerContext` directly.
744
+ */
745
+ declare function resolveViewerUserId(headers: Headers, env: ViewerEnv, options?: ResolveViewerOptions): Promise<string | null>;
746
+
585
747
  /**
586
748
  * Adapters between the marketplace wrapper's calling convention and an app's
587
749
  * inner handler signatures, with fail-closed per-actor authorization built in.
@@ -902,6 +1064,15 @@ interface AppApprovalEnvelope {
902
1064
  * retry silently discards the tap. See `approvalHash`.
903
1065
  */
904
1066
  hash?: string;
1067
+ /**
1068
+ * How many records this ONE call touches, as you count them. Required for a
1069
+ * workflow step's standing approval to cover the call on an unattended run
1070
+ * (sprigr-team decision 0039): the platform cannot see inside your write,
1071
+ * so an envelope with no count gets a person or nothing. The wrappers set
1072
+ * it to 1 for a single-target spec; a batch action supplies
1073
+ * `ApprovalSpec.count`.
1074
+ */
1075
+ count?: number;
905
1076
  }
906
1077
  /**
907
1078
  * Platform-stamped marker telling an app that a human approved this call. The
@@ -1105,6 +1276,14 @@ interface ApprovalSpec<P = unknown> {
1105
1276
  * Use `set()` / `seq()` from `approval-hash`. Never include `confirm`.
1106
1277
  */
1107
1278
  hash?: (args: ToolArgs) => Array<string | number | undefined | null>;
1279
+ /**
1280
+ * How many records this call touches, for `_approval.count`. Omit for a
1281
+ * single-target tool (the wrapper reports 1 when a `keys` id resolved, and
1282
+ * nothing when none did). A batch action (delete these ids, tag these
1283
+ * products) MUST supply it, or it reports nothing and no standing approval
1284
+ * can ever cover it, which is the safe side but not the useful one.
1285
+ */
1286
+ count?: (args: ToolArgs) => number;
1108
1287
  /** Present only for tools whose write can be (partly) reversed. */
1109
1288
  undo?: UndoCaptureSpec<P>;
1110
1289
  }
@@ -1940,4 +2119,4 @@ interface ExecutionContextLike {
1940
2119
  }
1941
2120
  type HandlerFn<TArgs, TResult> = (args: TArgs, env: Record<string, unknown>, ctx?: ExecutionContextLike) => Promise<TResult>;
1942
2121
 
1943
- export { APPROVAL_GRANTED_KEY, type Actor, type AppApprovalEnvelope, type AppFileListItem, type AppFileUrlArgs, type AppFileUrlResult, type AppFilesEnv, type AppUndoEnvelope, type ApplyPoliciesOptions, type ApprovalGateOptions, type ApprovalSpec, type ArchiveOfferOptions, type ArchiveOfferRefusal, type CaptureJournal, type ConfirmCondition, type ConfirmRule, type ConfirmationPolicy, type D1Like, type D1PreparedStatementLike, DEFAULT_EMIT_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 ManifestDispatchBlock, type ManifestLike, type ManifestToolLike, type MarketplaceEventEmitOpts, type MarketplaceEventSourceIntegration, NotConnectedError, type OAuthBouncerSignals, type OAuthState, type OfferUndoArgs, type PlatformHostEnv, type PolicyCheckInput, type PolicySource, type PutAppFileArgs, type PutAppFileResult, type PutAppFileStreamArgs, type RequireApprovalOptions, type RestoreResult, type RestoreSpec, type ScheduleArgs, type SprigrDataAggregationCell, type SprigrDataAggregationResult, type SprigrDataAggregationSpec, type SprigrDataApi, type SprigrDataIndexOpts, type SprigrDataSearchOpts, type SprigrDataSearchResult, type SprigrFilesApi, type SprigrFilesApiWithEdit, type SprigrFilesCreateInput, type SprigrFilesCreateResult, type SprigrFilesEditInput, type SprigrFilesEditResult, type SprigrFilesJobResult, type ToolArgs, type ToolHandler, type ToolResult, type ToolWrapperOptions, UNIT_SEP, type UndoApplyOptions, type UndoApplyResult, type UndoCaptureSpec, type UndoFidelity, type VerbRule, type WebhookArgs, type WfpBridgeEnv, type WfpEmitReply, actorKey, actorTool, appFileUrl, applyConfirmationPolicies, approvalHash, archiveOfferRefusal, base64ToBytes, buildConfirmationPolicy, buildMarketplaceWebhookUrl, bytesToBase64, canEmit, checkConfirmationPolicy, commonDestructiveVerbs, constantTimeEqual, createMarketplaceEmitter, createToolWrappers, decodeState, deleteAppFile, deriveConfirmationPolicy, describeMissingBridge, dispatcherApproval, emitMarketplaceEvent, encodeState, fetchFileAsBase64, fetchFileBytes, fetchWithRetry, fullWarning, getAppFile, hmacSha256Hex, installTokenPost, isNotConnectedError, isStagingHost, listAppFiles, manifestIsFresh, offerUndo, overlaySprigr, ownerFromActorKey, parseActor, putAppFile, putAppFileStream, randomHex, recreatedWarning, refuseWithoutForce, requireApproval, resolveInstallBridge, resolveOAuthBouncerBase, resolveOAuthBouncerCallbackUrl, resolvePlatformWebhookBase, runUndoApply, safeCapture, seq, serializeManifest, set, tool, undoEnvelope, withSprigrEmitFallback };
2122
+ export { APPROVAL_GRANTED_KEY, type Actor, type AppApprovalEnvelope, type AppFileListItem, type AppFileUrlArgs, type AppFileUrlResult, type AppFilesEnv, type AppUndoEnvelope, type ApplyPoliciesOptions, type ApprovalGateOptions, type ApprovalSpec, type ArchiveOfferOptions, type ArchiveOfferRefusal, type CaptureJournal, type ConfirmCondition, type ConfirmRule, type ConfirmationPolicy, type D1Like, type D1PreparedStatementLike, DEFAULT_EMIT_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 ManifestDispatchBlock, type ManifestLike, type ManifestToolLike, type MarketplaceEventEmitOpts, type MarketplaceEventSourceIntegration, NotConnectedError, type OAuthBouncerSignals, type OAuthState, type OfferUndoArgs, type PlatformHostEnv, type PolicyCheckInput, type PolicySource, type PutAppFileArgs, type PutAppFileResult, type PutAppFileStreamArgs, type RequireApprovalOptions, type ResolveViewerOptions, type RestoreResult, type RestoreSpec, type ScheduleArgs, type SprigrDataAggregationCell, type SprigrDataAggregationResult, type SprigrDataAggregationSpec, type SprigrDataApi, type SprigrDataIndexOpts, type SprigrDataSearchOpts, type SprigrDataSearchResult, type SprigrFilesApi, type SprigrFilesApiWithEdit, type SprigrFilesCreateInput, type SprigrFilesCreateResult, type SprigrFilesEditInput, type SprigrFilesEditResult, type SprigrFilesJobResult, 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, bytesToBase64, canEmit, checkConfirmationPolicy, commonDestructiveVerbs, constantTimeEqual, createMarketplaceEmitter, createToolWrappers, decodeState, deleteAppFile, deriveConfirmationPolicy, describeMissingBridge, dispatcherApproval, emitMarketplaceEvent, encodeState, fetchFileAsBase64, fetchFileBytes, fetchWithRetry, fullWarning, getAppFile, hmacSha256Hex, installTokenPost, isNotConnectedError, isStagingHost, listAppFiles, manifestIsFresh, offerUndo, overlaySprigr, ownerFromActorKey, parseActor, putAppFile, putAppFileStream, randomHex, recreatedWarning, refuseWithoutForce, requireApproval, resolveInstallBridge, resolveOAuthBouncerBase, resolveOAuthBouncerCallbackUrl, resolvePlatformWebhookBase, resolveViewerContext, resolveViewerUserId, runUndoApply, safeCapture, seq, serializeManifest, set, tool, undoEnvelope, verifyViewerToken, withSprigrEmitFallback };
package/dist/index.js CHANGED
@@ -339,6 +339,123 @@ function ownerFromActorKey(key) {
339
339
  return null;
340
340
  }
341
341
 
342
+ // src/viewer.ts
343
+ var VIEWER_CONTEXT_HEADER = "x-sprigr-viewer";
344
+ var VIEWER_PLATFORM_USER_ID_HEADER = "x-sprigr-platform-user-id";
345
+ var VIEWER_USER_ID_HEADER = "x-sprigr-user-id";
346
+ var VIEWER_ROLE_HEADER = "x-sprigr-role";
347
+ var VIEWER_ANONYMOUS = "anonymous";
348
+ var TOKEN_VERSION = "svc1";
349
+ var DEFAULT_CLOCK_SKEW_SECONDS = 60;
350
+ function base64urlDecode2(s) {
351
+ try {
352
+ let b64 = s.replace(/-/g, "+").replace(/_/g, "/");
353
+ while (b64.length % 4 !== 0) b64 += "=";
354
+ const bin = atob(b64);
355
+ const out = new Uint8Array(bin.length);
356
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
357
+ return out;
358
+ } catch {
359
+ return null;
360
+ }
361
+ }
362
+ async function hmacBytes(secret, message) {
363
+ const key = await crypto.subtle.importKey(
364
+ "raw",
365
+ new TextEncoder().encode(secret),
366
+ { name: "HMAC", hash: "SHA-256" },
367
+ false,
368
+ ["sign"]
369
+ );
370
+ return new Uint8Array(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message)));
371
+ }
372
+ function constantTimeEqualBytes(a, b) {
373
+ if (a.byteLength !== b.byteLength) return false;
374
+ let diff = 0;
375
+ for (let i = 0; i < a.byteLength; i++) diff |= a[i] ^ b[i];
376
+ return diff === 0;
377
+ }
378
+ async function verifyViewerToken(token, env, options = {}) {
379
+ if (!token) return { ok: false, reason: "missing" };
380
+ const secret = env.SPRIGR_VIEWER_SECRET;
381
+ if (!secret) return { ok: false, reason: "no_secret" };
382
+ const parts = token.split(".");
383
+ if (parts.length !== 3 || parts[0] !== TOKEN_VERSION) return { ok: false, reason: "malformed" };
384
+ const payloadB64 = parts[1];
385
+ const macB64 = parts[2];
386
+ const provided = base64urlDecode2(macB64);
387
+ if (!provided) return { ok: false, reason: "malformed" };
388
+ const expected = await hmacBytes(secret, `${TOKEN_VERSION}.${payloadB64}`);
389
+ if (!constantTimeEqualBytes(provided, expected)) return { ok: false, reason: "bad_signature" };
390
+ const payloadBytes = base64urlDecode2(payloadB64);
391
+ if (!payloadBytes) return { ok: false, reason: "malformed" };
392
+ let parsed;
393
+ try {
394
+ parsed = JSON.parse(new TextDecoder().decode(payloadBytes));
395
+ } catch {
396
+ return { ok: false, reason: "malformed" };
397
+ }
398
+ const p = parsed;
399
+ if (!p || typeof p !== "object" || typeof p.install_id !== "string" || !p.install_id || typeof p.company_id !== "string" || !p.company_id || typeof p.user_id !== "string" || !p.user_id || typeof p.platform_user_id !== "string" || !p.platform_user_id || typeof p.role !== "string" || !p.role || typeof p.issued_at !== "number" || !Number.isFinite(p.issued_at) || typeof p.expires_at !== "number" || !Number.isFinite(p.expires_at) || p.display_name !== void 0 && typeof p.display_name !== "string" || p.email !== void 0 && typeof p.email !== "string") {
400
+ return { ok: false, reason: "malformed" };
401
+ }
402
+ const now = options.nowSeconds ?? Math.floor(Date.now() / 1e3);
403
+ const skew = options.clockSkewSeconds ?? DEFAULT_CLOCK_SKEW_SECONDS;
404
+ if (now >= p.expires_at) return { ok: false, reason: "expired" };
405
+ if (p.issued_at - now > skew) return { ok: false, reason: "not_yet_valid" };
406
+ if (env.INSTALL_ID && p.install_id !== env.INSTALL_ID) {
407
+ return { ok: false, reason: "install_mismatch" };
408
+ }
409
+ return { ok: true, claims: p };
410
+ }
411
+ function anonymousOrValue(raw) {
412
+ const v = raw?.trim();
413
+ if (!v || v === VIEWER_ANONYMOUS) return null;
414
+ return v;
415
+ }
416
+ async function resolveViewerContext(headers, env, options = {}) {
417
+ const verified = await verifyViewerToken(headers.get(VIEWER_CONTEXT_HEADER), env, options);
418
+ if (verified.ok) {
419
+ const c = verified.claims;
420
+ const platformUserId2 = anonymousOrValue(c.platform_user_id);
421
+ return {
422
+ trust: "verified",
423
+ platformUserId: platformUserId2,
424
+ userId: anonymousOrValue(c.user_id),
425
+ companyId: c.company_id,
426
+ role: anonymousOrValue(c.role),
427
+ // Display fields are ignored for a signed-out viewer even if a future
428
+ // platform version were to send them: a name with no identity behind
429
+ // it has nobody to belong to.
430
+ displayName: platformUserId2 ? c.display_name ?? null : null,
431
+ email: platformUserId2 ? c.email ?? null : null,
432
+ installId: c.install_id
433
+ };
434
+ }
435
+ if (!options.allowTransportFallback) return null;
436
+ const platformUserId = anonymousOrValue(headers.get(VIEWER_PLATFORM_USER_ID_HEADER));
437
+ const userId = anonymousOrValue(headers.get(VIEWER_USER_ID_HEADER));
438
+ if (!platformUserId && !userId) return null;
439
+ return {
440
+ trust: "transport",
441
+ platformUserId,
442
+ userId,
443
+ companyId: null,
444
+ role: anonymousOrValue(headers.get(VIEWER_ROLE_HEADER)),
445
+ // The legacy channel never carries these.
446
+ displayName: null,
447
+ email: null,
448
+ installId: null
449
+ };
450
+ }
451
+ async function resolveViewerUserId(headers, env, options = {}) {
452
+ const viewer = await resolveViewerContext(headers, env, {
453
+ allowTransportFallback: true,
454
+ ...options
455
+ });
456
+ return viewer?.platformUserId ?? null;
457
+ }
458
+
342
459
  // src/tool-wrappers.ts
343
460
  var NotConnectedError = class extends Error {
344
461
  constructor(message = "The calling actor has no connection with this app.") {
@@ -614,6 +731,17 @@ function rawIdOf(args, keys) {
614
731
  }
615
732
  return "";
616
733
  }
734
+ function countOf(spec, params, rawId) {
735
+ if (spec.count) {
736
+ try {
737
+ const n = spec.count(params);
738
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : void 0;
739
+ } catch {
740
+ return void 0;
741
+ }
742
+ }
743
+ return rawId ? 1 : void 0;
744
+ }
617
745
  function isOk(result) {
618
746
  return !!result && typeof result === "object" && result.ok !== false;
619
747
  }
@@ -635,11 +763,13 @@ async function askPass(name, spec, params, env, opts, hashPrefix) {
635
763
  const pinned = await pin(opts, env, connection);
636
764
  const target = rawId && opts.describeTarget ? await safeLabel(opts.describeTarget, pinned, rawId, name) : rawId || "(unknown id)";
637
765
  const extra = spec.hash ? spec.hash(params) : [];
766
+ const count = countOf(spec, params, rawId);
638
767
  return {
639
768
  ok: false,
640
769
  _approval: {
641
770
  ...spec.describe(target, params, connection),
642
- hash: approvalHash(...hashPrefix, rawId, connection, ...extra)
771
+ hash: approvalHash(...hashPrefix, rawId, connection, ...extra),
772
+ ...count !== void 0 ? { count } : {}
643
773
  }
644
774
  };
645
775
  }
@@ -1041,6 +1171,11 @@ export {
1041
1171
  InvalidUndoEnvelopeError,
1042
1172
  NotConnectedError,
1043
1173
  UNIT_SEP,
1174
+ VIEWER_ANONYMOUS,
1175
+ VIEWER_CONTEXT_HEADER,
1176
+ VIEWER_PLATFORM_USER_ID_HEADER,
1177
+ VIEWER_ROLE_HEADER,
1178
+ VIEWER_USER_ID_HEADER,
1044
1179
  actorKey,
1045
1180
  actorTool,
1046
1181
  appFileUrl,
@@ -1089,6 +1224,8 @@ export {
1089
1224
  resolveOAuthBouncerBase,
1090
1225
  resolveOAuthBouncerCallbackUrl,
1091
1226
  resolvePlatformWebhookBase,
1227
+ resolveViewerContext,
1228
+ resolveViewerUserId,
1092
1229
  runUndoApply,
1093
1230
  safeCapture,
1094
1231
  seq,
@@ -1096,5 +1233,6 @@ export {
1096
1233
  set,
1097
1234
  tool,
1098
1235
  undoEnvelope,
1236
+ verifyViewerToken,
1099
1237
  withSprigrEmitFallback
1100
1238
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprigr/apps-app-sdk",
3
- "version": "0.8.7",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",