@sprigr/apps-app-sdk 0.8.8 → 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 +163 -1
- package/dist/index.js +125 -0
- package/package.json +1 -1
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.
|
|
@@ -1957,4 +2119,4 @@ interface ExecutionContextLike {
|
|
|
1957
2119
|
}
|
|
1958
2120
|
type HandlerFn<TArgs, TResult> = (args: TArgs, env: Record<string, unknown>, ctx?: ExecutionContextLike) => Promise<TResult>;
|
|
1959
2121
|
|
|
1960
|
-
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.") {
|
|
@@ -1054,6 +1171,11 @@ export {
|
|
|
1054
1171
|
InvalidUndoEnvelopeError,
|
|
1055
1172
|
NotConnectedError,
|
|
1056
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,
|
|
1057
1179
|
actorKey,
|
|
1058
1180
|
actorTool,
|
|
1059
1181
|
appFileUrl,
|
|
@@ -1102,6 +1224,8 @@ export {
|
|
|
1102
1224
|
resolveOAuthBouncerBase,
|
|
1103
1225
|
resolveOAuthBouncerCallbackUrl,
|
|
1104
1226
|
resolvePlatformWebhookBase,
|
|
1227
|
+
resolveViewerContext,
|
|
1228
|
+
resolveViewerUserId,
|
|
1105
1229
|
runUndoApply,
|
|
1106
1230
|
safeCapture,
|
|
1107
1231
|
seq,
|
|
@@ -1109,5 +1233,6 @@ export {
|
|
|
1109
1233
|
set,
|
|
1110
1234
|
tool,
|
|
1111
1235
|
undoEnvelope,
|
|
1236
|
+
verifyViewerToken,
|
|
1112
1237
|
withSprigrEmitFallback
|
|
1113
1238
|
};
|