@sprigr/apps-app-sdk 0.9.0 → 0.10.1

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 CHANGED
@@ -14,7 +14,8 @@ The runtime injects the `env.SPRIGR` host object **only on `/__sprigr/*` dispatc
14
14
  - `createMarketplaceEmitter(integrationType, defaults?)` — pre-bind an app's integration type so call sites pass only what varies; `sourceIntegration` is built per call from `env.INSTALL_ID`.
15
15
  - `canEmit(env)` — whether an emit could reach the platform by either transport. Gate work that only exists to feed an emit on this, not on `env.SPRIGR?.emit`.
16
16
  - `withSprigrEmitFallback(env)` — repair `env.SPRIGR.emit` once and leave existing call sites untouched. Matches the host object's contract (resolves `{ ok, eventId, queued }`, throws on non-2xx).
17
- - `resolveInstallBridge(env)` / `installTokenPost(bridge, path, body, opts?)` — build your own `/internal/wfp/*` fallback (collections, files, inbox) with the auth and error extraction handled.
17
+ - `logToPlatform(env, entry | entry[], opts?)`: durable app logging from either context (`env.SPRIGR.log` on dispatch, `POST ${SPRIGR_PLATFORM_BASE}/internal/wfp/log` from an inline route). Rows land in the platform's `system_logs` Analytics Engine dataset (90-day retention, scoped to the install's company, category prefixed with the app slug), so use it instead of a per-webhook or per-tick audit row in your D1: D1 bills every row written, AE does not. Validation throws synchronously on a cap violation (summary 256, detail 4096, metadata JSON 3840, category 64 matching `[A-Za-z0-9._:-]`, 50 entries per call; nothing is ever truncated); after that the promise never rejects and resolves `{ ok: true, written }` or `{ ok: false, error, status?, detail? }`. Pass `{ waitUntil: ctx.waitUntil.bind(ctx) }` on an inline route so the send outlives the response. `withSprigrLogFallback(env, opts?)` installs `env.SPRIGR.log` once for many call sites; `canLog(env)` gates work whose only purpose is a log row; the `SprigrLogEntry` / `SprigrLogFn` types let your env interface declare the host member.
18
+ - `resolveInstallBridge(env)` / `installTokenPost(bridge, path, body, opts?)`: build your own `/internal/wfp/*` fallback (collections, files, inbox) with the auth and error extraction handled. A non-2xx throws an `Error` whose `.status`, `.error` (the platform's code) and `.detail` are set alongside the message.
18
19
  - `overlaySprigr(env, sprigr)` — overlay a patched `SPRIGR` via `Object.create`. Never rebuild a dispatch-path env by spread: the real bindings live on the prototype and `SPRIGR` is non-enumerable, so `{ ...env }` yields an env whose `DB` is `undefined`.
19
20
 
20
21
  ## Per-actor tool wrappers
@@ -69,6 +70,7 @@ Three platform tiers, and they are not substitutes for each other. Full guide: [
69
70
 
70
71
  - `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
72
  - `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.
73
+ - 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
74
  - `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
75
  - `undoEnvelope({...})` throws on a blank field. The platform drops the whole envelope on a blank and mints nothing, silently.
74
76
  - `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
@@ -465,6 +465,190 @@ declare function createMarketplaceEmitter<E extends WfpBridgeEnv = WfpBridgeEnv>
465
465
  */
466
466
  declare function withSprigrEmitFallback<E extends WfpBridgeEnv & object>(env: E): E;
467
467
 
468
+ /**
469
+ * `env.SPRIGR.log`: durable app logging without a D1 audit table.
470
+ *
471
+ * The platform (sprigr-team#7154) exposes `env.SPRIGR.log(entry | entry[])`
472
+ * on every `/__sprigr/*` dispatch path. Each entry becomes one row in the
473
+ * platform's `system_logs` Analytics Engine dataset: 90-day retention,
474
+ * scoped to the install's company, `source = 'install'`, category prefixed
475
+ * with the app slug, metadata stamped with `app_slug` + `install_id`.
476
+ *
477
+ * Use it for every "we saw X" line a webhook, schedule tick, poll or tool
478
+ * call used to write into the app's own D1 as an audit row. D1 bills each
479
+ * row written (plus one per index), so a row per delivery is the largest
480
+ * D1 line item an app can produce (sprigr-apps#1519: one Shopify install
481
+ * wrote ~1.7M audit + dedup rows a day). AE has no per-row write bill.
482
+ *
483
+ * This module carries three things:
484
+ *
485
+ * 1. The TYPES of the host member (`SprigrLogEntry`, `SprigrLogFn`), so an
486
+ * app's env interface can declare `log` on its `SPRIGR` type.
487
+ * 2. `validateLogEntries`, a byte-for-byte mirror of the caps the host
488
+ * member and the platform route enforce. Caps are REJECTIONS, never
489
+ * truncation: an oversize entry throws before anything is sent.
490
+ * 3. The inline-route fallback (`logToPlatform`, `withSprigrLogFallback`).
491
+ * Inline Next.js route handlers never receive `env.SPRIGR`, so an app
492
+ * whose provider webhook lands on one has no `log` there. Same escape
493
+ * hatch as `emitMarketplaceEvent`: `POST ${SPRIGR_PLATFORM_BASE}/internal/wfp/log`
494
+ * with the `SPRIGR_INSTALL_TOKEN` bearer and `{ entries: [...] }`.
495
+ *
496
+ * Wire contract (workers/provisioning/src/wfp-log.ts in sprigr-team):
497
+ * 200 { ok: true, written }
498
+ * 400 { error, detail, index, field, max?, length? } with error one of
499
+ * summary_too_long, detail_too_long, metadata_too_large,
500
+ * too_many_entries, invalid_category, invalid_level, ...
501
+ * 413 body_too_large (over 512 KiB)
502
+ * 401 / 404 bad token / inactive install
503
+ */
504
+
505
+ /** Severity of one log row. Maps onto `system_logs.level`. */
506
+ type SprigrLogLevel = 'debug' | 'info' | 'warn' | 'error';
507
+ declare const SPRIGR_LOG_LEVELS: readonly SprigrLogLevel[];
508
+ /**
509
+ * One row for `env.SPRIGR.log`. Every string cap is enforced client-side
510
+ * (throw) AND platform-side (400); nothing is ever truncated.
511
+ */
512
+ interface SprigrLogEntry {
513
+ /** Required. */
514
+ level: SprigrLogLevel;
515
+ /**
516
+ * Required. 1-64 chars of `[A-Za-z0-9._:-]`, first char alphanumeric. The
517
+ * platform stores it as `<app_slug>.<category>` and it is the exact-match
518
+ * filter key when reading rows back, so make it a stable outcome name
519
+ * (`webhook.ok`, `webhook.duplicate`, `sync.tick`) and put the varying
520
+ * parts (topic, ids) in `metadata`.
521
+ */
522
+ category: string;
523
+ /** Required. 1-256 chars. One line a human can scan in a log viewer. */
524
+ summary: string;
525
+ /** Optional, at most 4096 chars. Reserve it for error text; bulk goes in `metadata`. */
526
+ detail?: string;
527
+ /** Optional plain object whose `JSON.stringify` is at most 3840 chars.
528
+ * The platform merges `{ app_slug, install_id }` over it (platform keys win). */
529
+ metadata?: Record<string, unknown>;
530
+ /** Optional, at most 128 chars. Defaults on the dispatch path to the
531
+ * dispatching actor's agent id. */
532
+ agent_id?: string;
533
+ /** Optional, at most 128 chars. Defaults on the platform to the ambient request trace. */
534
+ trace_id?: string;
535
+ }
536
+ /**
537
+ * What `env.SPRIGR.log` resolves to. The promise NEVER rejects: transport
538
+ * and platform failures come back as `{ ok: false }` (and a console.warn).
539
+ * Only a cap or shape violation throws, synchronously, before sending.
540
+ */
541
+ type SprigrLogResult = {
542
+ ok: true;
543
+ written: number;
544
+ } | {
545
+ ok: false;
546
+ error: string;
547
+ status?: number;
548
+ detail?: string;
549
+ };
550
+ /**
551
+ * The host member's signature. Declare it on your app's `SPRIGR` env type:
552
+ *
553
+ * interface MyEnv { SPRIGR?: { emit: ...; log?: SprigrLogFn } }
554
+ *
555
+ * Mark it optional: wrapper builds older than sprigr-team#7154 do not carry
556
+ * it, and `logToPlatform` degrades cleanly when it is absent.
557
+ */
558
+ type SprigrLogFn = (input: SprigrLogEntry | SprigrLogEntry[]) => Promise<SprigrLogResult>;
559
+ /**
560
+ * The caps, verbatim from the platform route (`WFP_LOG_*` in
561
+ * workers/provisioning/src/wfp-log.ts) and the wrapper's `log` member. The
562
+ * platform's 400 body echoes its live cap, so a mismatch here is
563
+ * self-describing rather than silent.
564
+ */
565
+ declare const SPRIGR_LOG_LIMITS: Readonly<{
566
+ maxEntriesPerCall: 50;
567
+ maxCategoryChars: 64;
568
+ categoryPattern: RegExp;
569
+ maxSummaryChars: 256;
570
+ maxDetailChars: 4096;
571
+ /** 4096-char metadata blob minus the platform's 256-char reserve for
572
+ * `app_slug` + `install_id`. */
573
+ maxMetadataChars: 3840;
574
+ maxIdChars: 128;
575
+ }>;
576
+ /** Same 5s ceiling as the emit fallback: a webhook ack matters more than a log row. */
577
+ declare const DEFAULT_LOG_TIMEOUT_MS = 5000;
578
+ /**
579
+ * Thrown by `validateLogEntries` (and therefore by `logToPlatform` and the
580
+ * fallback `log`) before anything is sent. Carries the same fields as the
581
+ * platform's 400 body so a caller can log or branch on them.
582
+ */
583
+ declare class SprigrLogValidationError extends Error {
584
+ readonly index: number;
585
+ readonly field: string;
586
+ readonly max?: number;
587
+ readonly length?: number;
588
+ constructor(message: string, fields: {
589
+ index: number;
590
+ field: string;
591
+ max?: number;
592
+ length?: number;
593
+ });
594
+ }
595
+ /**
596
+ * Validate one entry against the platform caps. Returns a fresh, normalised
597
+ * copy (unknown keys dropped, `undefined` optionals omitted) or throws
598
+ * `SprigrLogValidationError`. Pure; `index` only labels the error.
599
+ */
600
+ declare function validateLogEntry(raw: unknown, index?: number): SprigrLogEntry;
601
+ /**
602
+ * Normalise `entry | entry[]` to a validated array, or throw. One bad entry
603
+ * rejects the whole batch, matching the platform's all-or-nothing write.
604
+ */
605
+ declare function validateLogEntries(input: unknown): SprigrLogEntry[];
606
+ interface LogToPlatformOptions {
607
+ /** Override the 5s ceiling on the HTTP fallback. */
608
+ timeoutMs?: number;
609
+ /**
610
+ * `ctx.waitUntil` from the route handler. On the HTTP fallback the row
611
+ * is only guaranteed to land if the fetch outlives the response, which
612
+ * on an inline route means registering it here. The injected host member
613
+ * does this on its own; the fallback cannot see `ctx`, so hand it over.
614
+ */
615
+ waitUntil?: (p: Promise<unknown>) => void;
616
+ }
617
+ /**
618
+ * Whether a log row could reach the platform at all, by either transport.
619
+ * Gate work whose only purpose is to build a log entry on this, not on
620
+ * `env.SPRIGR?.log`, which is absent on every inline route.
621
+ */
622
+ declare function canLog(env: WfpBridgeEnv): boolean;
623
+ /**
624
+ * Write log rows from any execution context.
625
+ *
626
+ * Prefers the injected `env.SPRIGR.log`; falls back to the install-token
627
+ * bridge when it is absent (an inline Next route). Validation runs first
628
+ * and THROWS on a cap or shape violation, exactly like the host member, so
629
+ * a bad entry is caught at the call site. Everything after validation is
630
+ * reported in the result, never raised: a webhook ack is never at risk.
631
+ *
632
+ * await logToPlatform(env, {
633
+ * level: 'info', category: 'webhook.ok', summary: 'orders/create 6699',
634
+ * metadata: { topic: 'orders/create', resource_id: '6699' },
635
+ * }, { waitUntil: ctx.waitUntil.bind(ctx) });
636
+ */
637
+ declare function logToPlatform(env: WfpBridgeEnv, input: SprigrLogEntry | SprigrLogEntry[], opts?: LogToPlatformOptions): Promise<SprigrLogResult>;
638
+ /**
639
+ * Return an env whose `SPRIGR.log` works even on an inline route, leaving
640
+ * existing `env.SPRIGR.log(...)` call sites untouched. Same shape as
641
+ * `withSprigrEmitFallback`, and composable with it:
642
+ *
643
+ * const env = withSprigrLogFallback(withSprigrEmitFallback(rawEnv), { waitUntil });
644
+ *
645
+ * The installed `log` matches the host member's contract: it throws
646
+ * synchronously on a cap or shape violation and its promise never rejects.
647
+ * When the bindings are absent the env is returned as-is (`SPRIGR.log`
648
+ * stays undefined) so callers fail the same way they already do.
649
+ */
650
+ declare function withSprigrLogFallback<E extends WfpBridgeEnv & object>(env: E, opts?: LogToPlatformOptions): E;
651
+
468
652
  /**
469
653
  * Caller identity stamped by the platform on agent-initiated WFP dispatches.
470
654
  *
@@ -1250,12 +1434,25 @@ declare function refuseWithoutForce<E>(deleteAction: string, inner: ToolHandler<
1250
1434
  * `_undo` ON A NULL CAPTURE. A failed capture does not fail the write: the
1251
1435
  * user asked for it and a human approved it. It degrades to no token.
1252
1436
  *
1437
+ * Every resolver here is gate-level by default and overridable per spec:
1438
+ * `ApprovalSpec.resolveConnection` / `.describeTarget` / `.stampConnection`
1439
+ * win over the gate's for that spec alone. One rule cannot describe two
1440
+ * families of write truthfully, and both consumers of the resolved value are
1441
+ * safety surfaces (the card a person reads, and the grant hash).
1442
+ *
1253
1443
  * Generalised from the Shopify app's `require-approval.ts`; the per-tool
1254
1444
  * questions stay in the app, because only the app knows what cannot be walked
1255
1445
  * back.
1256
1446
  */
1257
- /** Per-tool (or per-action) spec: how the card reads, and (optionally) how to journal a before-image. */
1258
- interface ApprovalSpec<P = unknown> {
1447
+ /**
1448
+ * Per-tool (or per-action) spec: how the card reads, and (optionally) how to
1449
+ * journal a before-image.
1450
+ *
1451
+ * `P` is the pinned connection handed to `capture` / `describeTarget`; `E` is
1452
+ * the env a spec-level `resolveConnection` receives. `E` defaults to `unknown`
1453
+ * so `ApprovalSpec<MyState>` keeps working unchanged.
1454
+ */
1455
+ interface ApprovalSpec<P = unknown, E = unknown> {
1259
1456
  /**
1260
1457
  * Arg names that may carry the target id, most specific first. ONE list, used
1261
1458
  * for both the card label and the grant hash, so the two cannot drift apart
@@ -1284,6 +1481,27 @@ interface ApprovalSpec<P = unknown> {
1284
1481
  * can ever cover it, which is the safe side but not the useful one.
1285
1482
  */
1286
1483
  count?: (args: ToolArgs) => number;
1484
+ /**
1485
+ * Overrides the gate's `resolveConnection` for THIS spec only; the gate's
1486
+ * value stays the default for every spec that omits it.
1487
+ *
1488
+ * One gate serves every spec, so a gate-level resolver has to pick one rule.
1489
+ * That is wrong the moment an app has two families of write that reach a
1490
+ * connection differently: in meta-ads (sprigr-apps#1462) six creates POST
1491
+ * under an ad account and three address a Graph object by id on a path that
1492
+ * carries no account at all, so the account resolved for the creates was
1493
+ * printed on the delete's card and mixed into its hash while being false.
1494
+ * A resolver returning '' here is the honest answer for such a spec.
1495
+ */
1496
+ resolveConnection?: (env: E, args: ToolArgs) => Promise<string>;
1497
+ /** Overrides the gate's `describeTarget` for this spec only. */
1498
+ describeTarget?: (pinned: P, id: string, name: string) => Promise<string>;
1499
+ /**
1500
+ * Overrides the gate's `stampConnection` for this spec only. The stamp
1501
+ * repeats the card's claim in the agent's "done" report, so a spec that
1502
+ * resolves its own connection usually wants to stamp it its own way too.
1503
+ */
1504
+ stampConnection?: (result: unknown, connection: string) => unknown;
1287
1505
  /** Present only for tools whose write can be (partly) reversed. */
1288
1506
  undo?: UndoCaptureSpec<P>;
1289
1507
  }
@@ -1322,6 +1540,10 @@ interface ApprovalGateOptions<E, P = E> {
1322
1540
  * arguments and the install, canonicalised (a label like "EU store" becomes
1323
1541
  * the real domain). Return '' for a single-connection app. Never read it off
1324
1542
  * the arguments alone: on the retry the model usually omits it.
1543
+ *
1544
+ * This is the DEFAULT for every spec. A spec whose write reaches a
1545
+ * connection by another rule overrides it with
1546
+ * `ApprovalSpec.resolveConnection`.
1325
1547
  */
1326
1548
  resolveConnection: (env: E, args: ToolArgs) => Promise<string>;
1327
1549
  /**
@@ -1334,7 +1556,8 @@ interface ApprovalGateOptions<E, P = E> {
1334
1556
  * back to the id on any miss; never guess, a confidently wrong card is worse
1335
1557
  * than a terse one. Omit to always show the raw id. `name` is the tool, or
1336
1558
  * for a dispatcher the action, so a lookup can route by resource type when
1337
- * the id alone does not say what it is (an Asana gid).
1559
+ * the id alone does not say what it is (an Asana gid). Overridable per
1560
+ * spec.
1338
1561
  */
1339
1562
  describeTarget?: (pinned: P, id: string, name: string) => Promise<string>;
1340
1563
  /**
@@ -1345,7 +1568,8 @@ interface ApprovalGateOptions<E, P = E> {
1345
1568
  journal?: (env: E) => CaptureJournal;
1346
1569
  /**
1347
1570
  * Stamp the resolved connection onto a successful result (e.g. `{ store }`)
1348
- * so an agent reporting "deleted it" can name where. Optional.
1571
+ * so an agent reporting "deleted it" can name where. Optional, and
1572
+ * overridable per spec.
1349
1573
  */
1350
1574
  stampConnection?: (result: unknown, connection: string) => unknown;
1351
1575
  }
@@ -1382,7 +1606,7 @@ interface DispatcherApprovalGate<E> {
1382
1606
  * rename that quietly ungated a permanent delete is exactly the failure this
1383
1607
  * guard exists to prevent, and it would look identical to working.
1384
1608
  */
1385
- declare function requireApproval<E>(handlers: Record<string, ToolHandler<E>>, specs: Record<string, ApprovalSpec<E>>, opts: RequireApprovalOptions<E>): Record<string, ToolHandler<E>>;
1609
+ declare function requireApproval<E>(handlers: Record<string, ToolHandler<E>>, specs: Record<string, ApprovalSpec<E, E>>, opts: RequireApprovalOptions<E>): Record<string, ToolHandler<E>>;
1386
1610
  /**
1387
1611
  * The same gate for a dispatcher tool (one tool, many actions selected by an
1388
1612
  * input field). Build it once beside the action registry and call `run` from
@@ -1396,7 +1620,7 @@ declare function requireApproval<E>(handlers: Record<string, ToolHandler<E>>, sp
1396
1620
  * params are read from `opts.inputField` (default `input`), or from the
1397
1621
  * top-level args when that field is absent (the flat-verb envelope).
1398
1622
  */
1399
- declare function dispatcherApproval<E, P = E>(specs: Record<string, ApprovalSpec<P>>, opts: DispatcherApprovalOptions<E, P>): DispatcherApprovalGate<E>;
1623
+ declare function dispatcherApproval<E, P = E>(specs: Record<string, ApprovalSpec<P, E>>, opts: DispatcherApprovalOptions<E, P>): DispatcherApprovalGate<E>;
1400
1624
 
1401
1625
  /**
1402
1626
  * The reversal entry point every adopting app declares as `undo.reverse_tool`.
@@ -2119,4 +2343,4 @@ interface ExecutionContextLike {
2119
2343
  }
2120
2344
  type HandlerFn<TArgs, TResult> = (args: TArgs, env: Record<string, unknown>, ctx?: ExecutionContextLike) => Promise<TResult>;
2121
2345
 
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 };
2346
+ 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_LOG_TIMEOUT_MS, DEFAULT_MAX_FILE_BYTES, DEFAULT_MONEY_FIELDS, DEFAULT_WRITE_PREFIXES, type DerivePolicyOptions, type DerivedPolicy, type DispatcherApprovalGate, type DispatcherApprovalOptions, type EmitMarketplaceEventOptions, type EmitResult, type EmitTransport, type EventArgs, type ExecutionContextLike, type FetchFileResult, type FetchWithRetryOptions, type GetAppFileResult, type HandlerFn, type InboxOwner, type InstallBridge, InvalidUndoEnvelopeError, type JournalRowLike, type LogToPlatformOptions, type ManifestDispatchBlock, type ManifestLike, type ManifestToolLike, type MarketplaceEventEmitOpts, type MarketplaceEventSourceIntegration, NotConnectedError, type OAuthBouncerSignals, type OAuthState, type OfferUndoArgs, type PlatformHostEnv, type PolicyCheckInput, type PolicySource, type PutAppFileArgs, type PutAppFileResult, type PutAppFileStreamArgs, type RequireApprovalOptions, type ResolveViewerOptions, type RestoreResult, type RestoreSpec, SPRIGR_LOG_LEVELS, SPRIGR_LOG_LIMITS, 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 SprigrLogEntry, type SprigrLogFn, type SprigrLogLevel, type SprigrLogResult, SprigrLogValidationError, type ToolArgs, type ToolHandler, type ToolResult, type ToolWrapperOptions, UNIT_SEP, type UndoApplyOptions, type UndoApplyResult, type UndoCaptureSpec, type UndoFidelity, VIEWER_ANONYMOUS, VIEWER_CONTEXT_HEADER, VIEWER_PLATFORM_USER_ID_HEADER, VIEWER_ROLE_HEADER, VIEWER_USER_ID_HEADER, type VerbRule, type ViewerContext, type ViewerContextFailure, type ViewerContextVerification, type ViewerEnv, type WebhookArgs, type WfpBridgeEnv, type WfpEmitReply, actorKey, actorTool, appFileUrl, applyConfirmationPolicies, approvalHash, archiveOfferRefusal, base64ToBytes, buildConfirmationPolicy, buildMarketplaceWebhookUrl, bytesToBase64, canEmit, canLog, checkConfirmationPolicy, commonDestructiveVerbs, constantTimeEqual, createMarketplaceEmitter, createToolWrappers, decodeState, deleteAppFile, deriveConfirmationPolicy, describeMissingBridge, dispatcherApproval, emitMarketplaceEvent, encodeState, fetchFileAsBase64, fetchFileBytes, fetchWithRetry, fullWarning, getAppFile, hmacSha256Hex, installTokenPost, isNotConnectedError, isStagingHost, listAppFiles, logToPlatform, manifestIsFresh, offerUndo, overlaySprigr, ownerFromActorKey, parseActor, putAppFile, putAppFileStream, randomHex, recreatedWarning, refuseWithoutForce, requireApproval, resolveInstallBridge, resolveOAuthBouncerBase, resolveOAuthBouncerCallbackUrl, resolvePlatformWebhookBase, resolveViewerContext, resolveViewerUserId, runUndoApply, safeCapture, seq, serializeManifest, set, tool, undoEnvelope, validateLogEntries, validateLogEntry, verifyViewerToken, withSprigrEmitFallback, withSprigrLogFallback };
package/dist/index.js CHANGED
@@ -245,8 +245,13 @@ async function installTokenPost(bridge, path, body, opts) {
245
245
  parsed = null;
246
246
  }
247
247
  if (!resp.ok) {
248
- const detail = parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : text.slice(0, 200);
249
- throw new Error(`${label} failed: ${resp.status} ${detail}`);
248
+ const obj = parsed && typeof parsed === "object" ? parsed : null;
249
+ const code = obj && "error" in obj ? String(obj.error) : text.slice(0, 200);
250
+ const err = new Error(`${label} failed: ${resp.status} ${code}`);
251
+ err.status = resp.status;
252
+ err.error = code;
253
+ if (obj && typeof obj.detail === "string") err.detail = obj.detail;
254
+ throw err;
250
255
  }
251
256
  return parsed ?? {};
252
257
  } finally {
@@ -312,6 +317,198 @@ function withSprigrEmitFallback(env) {
312
317
  return overlaySprigr(env, { ...existing, emit });
313
318
  }
314
319
 
320
+ // src/platform-log.ts
321
+ var SPRIGR_LOG_LEVELS = ["debug", "info", "warn", "error"];
322
+ var SPRIGR_LOG_LIMITS = Object.freeze({
323
+ maxEntriesPerCall: 50,
324
+ maxCategoryChars: 64,
325
+ categoryPattern: /^[A-Za-z0-9][A-Za-z0-9._:-]*$/,
326
+ maxSummaryChars: 256,
327
+ maxDetailChars: 4096,
328
+ /** 4096-char metadata blob minus the platform's 256-char reserve for
329
+ * `app_slug` + `install_id`. */
330
+ maxMetadataChars: 3840,
331
+ maxIdChars: 128
332
+ });
333
+ var DEFAULT_LOG_TIMEOUT_MS = 5e3;
334
+ var SprigrLogValidationError = class extends Error {
335
+ index;
336
+ field;
337
+ max;
338
+ length;
339
+ constructor(message, fields) {
340
+ super(message);
341
+ this.name = "SprigrLogValidationError";
342
+ this.index = fields.index;
343
+ this.field = fields.field;
344
+ if (fields.max !== void 0) this.max = fields.max;
345
+ if (fields.length !== void 0) this.length = fields.length;
346
+ }
347
+ };
348
+ function isPlainObject(v) {
349
+ return !!v && typeof v === "object" && !Array.isArray(v);
350
+ }
351
+ function tooLong(index, field, length, max) {
352
+ return new SprigrLogValidationError(
353
+ `env.SPRIGR.log: entries[${index}].${field} is ${length} chars; max ${max}. Nothing was sent; the platform never truncates a log field. Move the long part into detail or metadata, or split it across rows.`,
354
+ { index, field, max, length }
355
+ );
356
+ }
357
+ function validateLogEntry(raw, index = 0) {
358
+ const at = `env.SPRIGR.log: entries[${index}]`;
359
+ if (!isPlainObject(raw)) {
360
+ throw new SprigrLogValidationError(`${at} must be an object`, { index, field: "entry" });
361
+ }
362
+ const L = SPRIGR_LOG_LIMITS;
363
+ const level = raw.level;
364
+ if (typeof level !== "string" || !SPRIGR_LOG_LEVELS.includes(level)) {
365
+ throw new SprigrLogValidationError(`${at}.level must be one of ${SPRIGR_LOG_LEVELS.join(" | ")}`, {
366
+ index,
367
+ field: "level"
368
+ });
369
+ }
370
+ const category = raw.category;
371
+ if (typeof category !== "string" || category.length === 0) {
372
+ throw new SprigrLogValidationError(`${at}.category required (string)`, { index, field: "category" });
373
+ }
374
+ if (category.length > L.maxCategoryChars) throw tooLong(index, "category", category.length, L.maxCategoryChars);
375
+ if (!L.categoryPattern.test(category)) {
376
+ throw new SprigrLogValidationError(
377
+ `${at}.category must match ${L.categoryPattern} (letters, digits, . _ : -; no spaces)`,
378
+ { index, field: "category" }
379
+ );
380
+ }
381
+ const summary = raw.summary;
382
+ if (typeof summary !== "string" || summary.length === 0) {
383
+ throw new SprigrLogValidationError(`${at}.summary required (string)`, { index, field: "summary" });
384
+ }
385
+ if (summary.length > L.maxSummaryChars) throw tooLong(index, "summary", summary.length, L.maxSummaryChars);
386
+ const out = { level, category, summary };
387
+ if (raw.detail !== void 0) {
388
+ if (typeof raw.detail !== "string") {
389
+ throw new SprigrLogValidationError(`${at}.detail must be a string`, { index, field: "detail" });
390
+ }
391
+ if (raw.detail.length > L.maxDetailChars) throw tooLong(index, "detail", raw.detail.length, L.maxDetailChars);
392
+ out.detail = raw.detail;
393
+ }
394
+ if (raw.metadata !== void 0) {
395
+ if (!isPlainObject(raw.metadata)) {
396
+ throw new SprigrLogValidationError(`${at}.metadata must be a plain object`, { index, field: "metadata" });
397
+ }
398
+ const serialised = JSON.stringify(raw.metadata).length;
399
+ if (serialised > L.maxMetadataChars) {
400
+ throw new SprigrLogValidationError(
401
+ `${at}.metadata JSON is ${serialised} chars; max ${L.maxMetadataChars}. Nothing was sent; drop or split fields.`,
402
+ { index, field: "metadata", max: L.maxMetadataChars, length: serialised }
403
+ );
404
+ }
405
+ out.metadata = raw.metadata;
406
+ }
407
+ for (const field of ["agent_id", "trace_id"]) {
408
+ const v = raw[field];
409
+ if (v === void 0) continue;
410
+ if (typeof v !== "string" || v.length === 0 || v.length > L.maxIdChars) {
411
+ throw new SprigrLogValidationError(
412
+ `${at}.${field} must be a non-empty string of at most ${L.maxIdChars} chars`,
413
+ { index, field, max: L.maxIdChars, length: typeof v === "string" ? v.length : void 0 }
414
+ );
415
+ }
416
+ out[field] = v;
417
+ }
418
+ return out;
419
+ }
420
+ function validateLogEntries(input) {
421
+ const list = Array.isArray(input) ? input : [input];
422
+ if (list.length === 0) {
423
+ throw new SprigrLogValidationError("env.SPRIGR.log: at least one entry required", {
424
+ index: 0,
425
+ field: "entries"
426
+ });
427
+ }
428
+ if (list.length > SPRIGR_LOG_LIMITS.maxEntriesPerCall) {
429
+ throw new SprigrLogValidationError(
430
+ `env.SPRIGR.log: at most ${SPRIGR_LOG_LIMITS.maxEntriesPerCall} entries per call (got ${list.length}); nothing was sent`,
431
+ {
432
+ index: 0,
433
+ field: "entries",
434
+ max: SPRIGR_LOG_LIMITS.maxEntriesPerCall,
435
+ length: list.length
436
+ }
437
+ );
438
+ }
439
+ return list.map((e, i) => validateLogEntry(e, i));
440
+ }
441
+ function bindingLog(env) {
442
+ const sprigr = env.SPRIGR;
443
+ return typeof sprigr?.log === "function" ? sprigr.log.bind(sprigr) : null;
444
+ }
445
+ function canLog(env) {
446
+ return bindingLog(env) !== null || resolveInstallBridge(env) !== null;
447
+ }
448
+ async function postLog(env, entries, timeoutMs) {
449
+ const bridge = resolveInstallBridge(env);
450
+ if (!bridge) {
451
+ return { ok: false, error: "no_log_path", detail: describeMissingBridge(env) };
452
+ }
453
+ try {
454
+ const reply = await installTokenPost(bridge, "/internal/wfp/log", { entries }, {
455
+ label: "log",
456
+ timeoutMs
457
+ });
458
+ if (reply.ok === false) {
459
+ return { ok: false, error: reply.error ?? "log_failed", detail: reply.detail };
460
+ }
461
+ return { ok: true, written: typeof reply.written === "number" ? reply.written : entries.length };
462
+ } catch (err) {
463
+ const e = err;
464
+ const message = e instanceof Error ? e.message : String(err);
465
+ console.warn(`[sprigr-log] log fallback failed: ${message}`);
466
+ return {
467
+ ok: false,
468
+ error: e.error ?? (e.status !== void 0 ? "log_failed" : "log_transport_failed"),
469
+ ...e.status !== void 0 ? { status: e.status } : {},
470
+ detail: e.detail ?? message
471
+ };
472
+ }
473
+ }
474
+ function logToPlatform(env, input, opts) {
475
+ const entries = validateLogEntries(input);
476
+ const injected = bindingLog(env);
477
+ let result;
478
+ if (injected) {
479
+ try {
480
+ result = Promise.resolve(injected(entries)).catch((err) => ({
481
+ ok: false,
482
+ error: "log_failed",
483
+ detail: err instanceof Error ? err.message : String(err)
484
+ }));
485
+ } catch (err) {
486
+ result = Promise.resolve({
487
+ ok: false,
488
+ error: "log_failed",
489
+ detail: err instanceof Error ? err.message : String(err)
490
+ });
491
+ }
492
+ } else {
493
+ result = postLog(env, entries, opts?.timeoutMs ?? DEFAULT_LOG_TIMEOUT_MS);
494
+ }
495
+ if (opts?.waitUntil) opts.waitUntil(result);
496
+ return result;
497
+ }
498
+ function withSprigrLogFallback(env, opts) {
499
+ if (bindingLog(env)) return env;
500
+ const bridge = resolveInstallBridge(env);
501
+ if (!bridge) return env;
502
+ const log = (input) => {
503
+ const entries = validateLogEntries(input);
504
+ const p = postLog(env, entries, opts?.timeoutMs ?? DEFAULT_LOG_TIMEOUT_MS);
505
+ if (opts?.waitUntil) opts.waitUntil(p);
506
+ return p;
507
+ };
508
+ const existing = env.SPRIGR ?? {};
509
+ return overlaySprigr(env, { ...existing, log });
510
+ }
511
+
315
512
  // src/actor.ts
316
513
  function parseActor(args) {
317
514
  if (!args || typeof args !== "object") return void 0;
@@ -758,10 +955,11 @@ async function pin(opts, env, connection) {
758
955
  return opts.pinEnv(env, connection);
759
956
  }
760
957
  async function askPass(name, spec, params, env, opts, hashPrefix) {
761
- const connection = await opts.resolveConnection(env, params);
958
+ const connection = await (spec.resolveConnection ?? opts.resolveConnection)(env, params);
762
959
  const rawId = rawIdOf(params, spec.keys);
763
960
  const pinned = await pin(opts, env, connection);
764
- const target = rawId && opts.describeTarget ? await safeLabel(opts.describeTarget, pinned, rawId, name) : rawId || "(unknown id)";
961
+ const describeTarget = spec.describeTarget ?? opts.describeTarget;
962
+ const target = rawId && describeTarget ? await safeLabel(describeTarget, pinned, rawId, name) : rawId || "(unknown id)";
765
963
  const extra = spec.hash ? spec.hash(params) : [];
766
964
  const count = countOf(spec, params, rawId);
767
965
  return {
@@ -774,8 +972,9 @@ async function askPass(name, spec, params, env, opts, hashPrefix) {
774
972
  };
775
973
  }
776
974
  async function grantedPass(name, spec, params, env, opts, write) {
777
- const connection = await opts.resolveConnection(env, params);
778
- const stamp = (r) => connection && opts.stampConnection ? opts.stampConnection(r, connection) : r;
975
+ const connection = await (spec.resolveConnection ?? opts.resolveConnection)(env, params);
976
+ const stampConnection = spec.stampConnection ?? opts.stampConnection;
977
+ const stamp = (r) => connection && stampConnection ? stampConnection(r, connection) : r;
779
978
  const undo = spec.undo;
780
979
  const id = rawIdOf(params, spec.keys);
781
980
  if (!undo || !id) return stamp(await write());
@@ -1165,11 +1364,15 @@ function commonDestructiveVerbs(subject) {
1165
1364
  export {
1166
1365
  APPROVAL_GRANTED_KEY,
1167
1366
  DEFAULT_EMIT_TIMEOUT_MS,
1367
+ DEFAULT_LOG_TIMEOUT_MS,
1168
1368
  DEFAULT_MAX_FILE_BYTES,
1169
1369
  DEFAULT_MONEY_FIELDS,
1170
1370
  DEFAULT_WRITE_PREFIXES,
1171
1371
  InvalidUndoEnvelopeError,
1172
1372
  NotConnectedError,
1373
+ SPRIGR_LOG_LEVELS,
1374
+ SPRIGR_LOG_LIMITS,
1375
+ SprigrLogValidationError,
1173
1376
  UNIT_SEP,
1174
1377
  VIEWER_ANONYMOUS,
1175
1378
  VIEWER_CONTEXT_HEADER,
@@ -1187,6 +1390,7 @@ export {
1187
1390
  buildMarketplaceWebhookUrl,
1188
1391
  bytesToBase64,
1189
1392
  canEmit,
1393
+ canLog,
1190
1394
  checkConfirmationPolicy,
1191
1395
  commonDestructiveVerbs,
1192
1396
  constantTimeEqual,
@@ -1209,6 +1413,7 @@ export {
1209
1413
  isNotConnectedError,
1210
1414
  isStagingHost,
1211
1415
  listAppFiles,
1416
+ logToPlatform,
1212
1417
  manifestIsFresh,
1213
1418
  offerUndo,
1214
1419
  overlaySprigr,
@@ -1233,6 +1438,9 @@ export {
1233
1438
  set,
1234
1439
  tool,
1235
1440
  undoEnvelope,
1441
+ validateLogEntries,
1442
+ validateLogEntry,
1236
1443
  verifyViewerToken,
1237
- withSprigrEmitFallback
1444
+ withSprigrEmitFallback,
1445
+ withSprigrLogFallback
1238
1446
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprigr/apps-app-sdk",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",