@sprigr/apps-app-sdk 0.8.8 → 0.10.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/README.md +1 -0
- package/dist/index.d.ts +209 -7
- package/dist/index.js +131 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,6 +69,7 @@ Three platform tiers, and they are not substitutes for each other. Full guide: [
|
|
|
69
69
|
|
|
70
70
|
- `requireApproval(handlers, specs, opts)` wraps the named tools so each returns an `_approval` card instead of writing, then on the granted pass captures a before-image through the **pinned** connection and offers `_undo`. It resolves the connection inside itself, before the label lookup, the hash and the capture: a gate that sits outside the app's own pin names the wrong store, hashes the wrong store and captures the wrong store's copy. Register the result after the originals: `Object.assign(registry, requireApproval(registry, SPECS, opts))`.
|
|
71
71
|
- `dispatcherApproval(specs, opts)`: the same gate for a one-tool-many-actions dispatcher. Build it beside the action registry and call `gate.run(action, args, env, () => def.execute(state, parsed))` from the dispatcher; params are read from `opts.inputField` (default `input`), the action name is hashed in, and `pinEnv` may return a per-actor token state rather than the env.
|
|
72
|
+
- Every resolver on the gate is a **default, overridable per spec**: `ApprovalSpec.resolveConnection`, `.describeTarget` and `.stampConnection` win over the gate's for that spec alone. Reach for it when one app has two families of write that reach a connection differently (creates that POST under an account, deletes that address a global object id on a path carrying no account). One gate-level rule has to pick one of them, and for the other it prints a false connection on the card a person taps and mixes it into the grant hash. A spec that hits no connection returns `''`.
|
|
72
73
|
- `approvalHash(rawId, connection, ...parts)`, `set(values)`, `seq(values)`: a stable operation identity so a tap survives the model reordering a list or dropping an optional argument on the retry.
|
|
73
74
|
- `undoEnvelope({...})` throws on a blank field. The platform drops the whole envelope on a blank and mints nothing, silently.
|
|
74
75
|
- `refuseWithoutForce(name, handler, opts)` / `archiveOfferRefusal(...)`: a delete refuses once and offers the vendor's native archive unless `force: true`.
|
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.
|
|
@@ -1088,12 +1250,25 @@ declare function refuseWithoutForce<E>(deleteAction: string, inner: ToolHandler<
|
|
|
1088
1250
|
* `_undo` ON A NULL CAPTURE. A failed capture does not fail the write: the
|
|
1089
1251
|
* user asked for it and a human approved it. It degrades to no token.
|
|
1090
1252
|
*
|
|
1253
|
+
* Every resolver here is gate-level by default and overridable per spec:
|
|
1254
|
+
* `ApprovalSpec.resolveConnection` / `.describeTarget` / `.stampConnection`
|
|
1255
|
+
* win over the gate's for that spec alone. One rule cannot describe two
|
|
1256
|
+
* families of write truthfully, and both consumers of the resolved value are
|
|
1257
|
+
* safety surfaces (the card a person reads, and the grant hash).
|
|
1258
|
+
*
|
|
1091
1259
|
* Generalised from the Shopify app's `require-approval.ts`; the per-tool
|
|
1092
1260
|
* questions stay in the app, because only the app knows what cannot be walked
|
|
1093
1261
|
* back.
|
|
1094
1262
|
*/
|
|
1095
|
-
/**
|
|
1096
|
-
|
|
1263
|
+
/**
|
|
1264
|
+
* Per-tool (or per-action) spec: how the card reads, and (optionally) how to
|
|
1265
|
+
* journal a before-image.
|
|
1266
|
+
*
|
|
1267
|
+
* `P` is the pinned connection handed to `capture` / `describeTarget`; `E` is
|
|
1268
|
+
* the env a spec-level `resolveConnection` receives. `E` defaults to `unknown`
|
|
1269
|
+
* so `ApprovalSpec<MyState>` keeps working unchanged.
|
|
1270
|
+
*/
|
|
1271
|
+
interface ApprovalSpec<P = unknown, E = unknown> {
|
|
1097
1272
|
/**
|
|
1098
1273
|
* Arg names that may carry the target id, most specific first. ONE list, used
|
|
1099
1274
|
* for both the card label and the grant hash, so the two cannot drift apart
|
|
@@ -1122,6 +1297,27 @@ interface ApprovalSpec<P = unknown> {
|
|
|
1122
1297
|
* can ever cover it, which is the safe side but not the useful one.
|
|
1123
1298
|
*/
|
|
1124
1299
|
count?: (args: ToolArgs) => number;
|
|
1300
|
+
/**
|
|
1301
|
+
* Overrides the gate's `resolveConnection` for THIS spec only; the gate's
|
|
1302
|
+
* value stays the default for every spec that omits it.
|
|
1303
|
+
*
|
|
1304
|
+
* One gate serves every spec, so a gate-level resolver has to pick one rule.
|
|
1305
|
+
* That is wrong the moment an app has two families of write that reach a
|
|
1306
|
+
* connection differently: in meta-ads (sprigr-apps#1462) six creates POST
|
|
1307
|
+
* under an ad account and three address a Graph object by id on a path that
|
|
1308
|
+
* carries no account at all, so the account resolved for the creates was
|
|
1309
|
+
* printed on the delete's card and mixed into its hash while being false.
|
|
1310
|
+
* A resolver returning '' here is the honest answer for such a spec.
|
|
1311
|
+
*/
|
|
1312
|
+
resolveConnection?: (env: E, args: ToolArgs) => Promise<string>;
|
|
1313
|
+
/** Overrides the gate's `describeTarget` for this spec only. */
|
|
1314
|
+
describeTarget?: (pinned: P, id: string, name: string) => Promise<string>;
|
|
1315
|
+
/**
|
|
1316
|
+
* Overrides the gate's `stampConnection` for this spec only. The stamp
|
|
1317
|
+
* repeats the card's claim in the agent's "done" report, so a spec that
|
|
1318
|
+
* resolves its own connection usually wants to stamp it its own way too.
|
|
1319
|
+
*/
|
|
1320
|
+
stampConnection?: (result: unknown, connection: string) => unknown;
|
|
1125
1321
|
/** Present only for tools whose write can be (partly) reversed. */
|
|
1126
1322
|
undo?: UndoCaptureSpec<P>;
|
|
1127
1323
|
}
|
|
@@ -1160,6 +1356,10 @@ interface ApprovalGateOptions<E, P = E> {
|
|
|
1160
1356
|
* arguments and the install, canonicalised (a label like "EU store" becomes
|
|
1161
1357
|
* the real domain). Return '' for a single-connection app. Never read it off
|
|
1162
1358
|
* the arguments alone: on the retry the model usually omits it.
|
|
1359
|
+
*
|
|
1360
|
+
* This is the DEFAULT for every spec. A spec whose write reaches a
|
|
1361
|
+
* connection by another rule overrides it with
|
|
1362
|
+
* `ApprovalSpec.resolveConnection`.
|
|
1163
1363
|
*/
|
|
1164
1364
|
resolveConnection: (env: E, args: ToolArgs) => Promise<string>;
|
|
1165
1365
|
/**
|
|
@@ -1172,7 +1372,8 @@ interface ApprovalGateOptions<E, P = E> {
|
|
|
1172
1372
|
* back to the id on any miss; never guess, a confidently wrong card is worse
|
|
1173
1373
|
* than a terse one. Omit to always show the raw id. `name` is the tool, or
|
|
1174
1374
|
* for a dispatcher the action, so a lookup can route by resource type when
|
|
1175
|
-
* the id alone does not say what it is (an Asana gid).
|
|
1375
|
+
* the id alone does not say what it is (an Asana gid). Overridable per
|
|
1376
|
+
* spec.
|
|
1176
1377
|
*/
|
|
1177
1378
|
describeTarget?: (pinned: P, id: string, name: string) => Promise<string>;
|
|
1178
1379
|
/**
|
|
@@ -1183,7 +1384,8 @@ interface ApprovalGateOptions<E, P = E> {
|
|
|
1183
1384
|
journal?: (env: E) => CaptureJournal;
|
|
1184
1385
|
/**
|
|
1185
1386
|
* Stamp the resolved connection onto a successful result (e.g. `{ store }`)
|
|
1186
|
-
* so an agent reporting "deleted it" can name where. Optional
|
|
1387
|
+
* so an agent reporting "deleted it" can name where. Optional, and
|
|
1388
|
+
* overridable per spec.
|
|
1187
1389
|
*/
|
|
1188
1390
|
stampConnection?: (result: unknown, connection: string) => unknown;
|
|
1189
1391
|
}
|
|
@@ -1220,7 +1422,7 @@ interface DispatcherApprovalGate<E> {
|
|
|
1220
1422
|
* rename that quietly ungated a permanent delete is exactly the failure this
|
|
1221
1423
|
* guard exists to prevent, and it would look identical to working.
|
|
1222
1424
|
*/
|
|
1223
|
-
declare function requireApproval<E>(handlers: Record<string, ToolHandler<E>>, specs: Record<string, ApprovalSpec<E>>, opts: RequireApprovalOptions<E>): Record<string, ToolHandler<E>>;
|
|
1425
|
+
declare function requireApproval<E>(handlers: Record<string, ToolHandler<E>>, specs: Record<string, ApprovalSpec<E, E>>, opts: RequireApprovalOptions<E>): Record<string, ToolHandler<E>>;
|
|
1224
1426
|
/**
|
|
1225
1427
|
* The same gate for a dispatcher tool (one tool, many actions selected by an
|
|
1226
1428
|
* input field). Build it once beside the action registry and call `run` from
|
|
@@ -1234,7 +1436,7 @@ declare function requireApproval<E>(handlers: Record<string, ToolHandler<E>>, sp
|
|
|
1234
1436
|
* params are read from `opts.inputField` (default `input`), or from the
|
|
1235
1437
|
* top-level args when that field is absent (the flat-verb envelope).
|
|
1236
1438
|
*/
|
|
1237
|
-
declare function dispatcherApproval<E, P = E>(specs: Record<string, ApprovalSpec<P>>, opts: DispatcherApprovalOptions<E, P>): DispatcherApprovalGate<E>;
|
|
1439
|
+
declare function dispatcherApproval<E, P = E>(specs: Record<string, ApprovalSpec<P, E>>, opts: DispatcherApprovalOptions<E, P>): DispatcherApprovalGate<E>;
|
|
1238
1440
|
|
|
1239
1441
|
/**
|
|
1240
1442
|
* The reversal entry point every adopting app declares as `undo.reverse_tool`.
|
|
@@ -1957,4 +2159,4 @@ interface ExecutionContextLike {
|
|
|
1957
2159
|
}
|
|
1958
2160
|
type HandlerFn<TArgs, TResult> = (args: TArgs, env: Record<string, unknown>, ctx?: ExecutionContextLike) => Promise<TResult>;
|
|
1959
2161
|
|
|
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 };
|
|
2162
|
+
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.") {
|
|
@@ -641,10 +758,11 @@ async function pin(opts, env, connection) {
|
|
|
641
758
|
return opts.pinEnv(env, connection);
|
|
642
759
|
}
|
|
643
760
|
async function askPass(name, spec, params, env, opts, hashPrefix) {
|
|
644
|
-
const connection = await opts.resolveConnection(env, params);
|
|
761
|
+
const connection = await (spec.resolveConnection ?? opts.resolveConnection)(env, params);
|
|
645
762
|
const rawId = rawIdOf(params, spec.keys);
|
|
646
763
|
const pinned = await pin(opts, env, connection);
|
|
647
|
-
const
|
|
764
|
+
const describeTarget = spec.describeTarget ?? opts.describeTarget;
|
|
765
|
+
const target = rawId && describeTarget ? await safeLabel(describeTarget, pinned, rawId, name) : rawId || "(unknown id)";
|
|
648
766
|
const extra = spec.hash ? spec.hash(params) : [];
|
|
649
767
|
const count = countOf(spec, params, rawId);
|
|
650
768
|
return {
|
|
@@ -657,8 +775,9 @@ async function askPass(name, spec, params, env, opts, hashPrefix) {
|
|
|
657
775
|
};
|
|
658
776
|
}
|
|
659
777
|
async function grantedPass(name, spec, params, env, opts, write) {
|
|
660
|
-
const connection = await opts.resolveConnection(env, params);
|
|
661
|
-
const
|
|
778
|
+
const connection = await (spec.resolveConnection ?? opts.resolveConnection)(env, params);
|
|
779
|
+
const stampConnection = spec.stampConnection ?? opts.stampConnection;
|
|
780
|
+
const stamp = (r) => connection && stampConnection ? stampConnection(r, connection) : r;
|
|
662
781
|
const undo = spec.undo;
|
|
663
782
|
const id = rawIdOf(params, spec.keys);
|
|
664
783
|
if (!undo || !id) return stamp(await write());
|
|
@@ -1054,6 +1173,11 @@ export {
|
|
|
1054
1173
|
InvalidUndoEnvelopeError,
|
|
1055
1174
|
NotConnectedError,
|
|
1056
1175
|
UNIT_SEP,
|
|
1176
|
+
VIEWER_ANONYMOUS,
|
|
1177
|
+
VIEWER_CONTEXT_HEADER,
|
|
1178
|
+
VIEWER_PLATFORM_USER_ID_HEADER,
|
|
1179
|
+
VIEWER_ROLE_HEADER,
|
|
1180
|
+
VIEWER_USER_ID_HEADER,
|
|
1057
1181
|
actorKey,
|
|
1058
1182
|
actorTool,
|
|
1059
1183
|
appFileUrl,
|
|
@@ -1102,6 +1226,8 @@ export {
|
|
|
1102
1226
|
resolveOAuthBouncerBase,
|
|
1103
1227
|
resolveOAuthBouncerCallbackUrl,
|
|
1104
1228
|
resolvePlatformWebhookBase,
|
|
1229
|
+
resolveViewerContext,
|
|
1230
|
+
resolveViewerUserId,
|
|
1105
1231
|
runUndoApply,
|
|
1106
1232
|
safeCapture,
|
|
1107
1233
|
seq,
|
|
@@ -1109,5 +1235,6 @@ export {
|
|
|
1109
1235
|
set,
|
|
1110
1236
|
tool,
|
|
1111
1237
|
undoEnvelope,
|
|
1238
|
+
verifyViewerToken,
|
|
1112
1239
|
withSprigrEmitFallback
|
|
1113
1240
|
};
|