@sprigr/apps-app-sdk 0.10.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
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
  *
@@ -2159,4 +2343,4 @@ interface ExecutionContextLike {
2159
2343
  }
2160
2344
  type HandlerFn<TArgs, TResult> = (args: TArgs, env: Record<string, unknown>, ctx?: ExecutionContextLike) => Promise<TResult>;
2161
2345
 
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 };
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;
@@ -1167,11 +1364,15 @@ function commonDestructiveVerbs(subject) {
1167
1364
  export {
1168
1365
  APPROVAL_GRANTED_KEY,
1169
1366
  DEFAULT_EMIT_TIMEOUT_MS,
1367
+ DEFAULT_LOG_TIMEOUT_MS,
1170
1368
  DEFAULT_MAX_FILE_BYTES,
1171
1369
  DEFAULT_MONEY_FIELDS,
1172
1370
  DEFAULT_WRITE_PREFIXES,
1173
1371
  InvalidUndoEnvelopeError,
1174
1372
  NotConnectedError,
1373
+ SPRIGR_LOG_LEVELS,
1374
+ SPRIGR_LOG_LIMITS,
1375
+ SprigrLogValidationError,
1175
1376
  UNIT_SEP,
1176
1377
  VIEWER_ANONYMOUS,
1177
1378
  VIEWER_CONTEXT_HEADER,
@@ -1189,6 +1390,7 @@ export {
1189
1390
  buildMarketplaceWebhookUrl,
1190
1391
  bytesToBase64,
1191
1392
  canEmit,
1393
+ canLog,
1192
1394
  checkConfirmationPolicy,
1193
1395
  commonDestructiveVerbs,
1194
1396
  constantTimeEqual,
@@ -1211,6 +1413,7 @@ export {
1211
1413
  isNotConnectedError,
1212
1414
  isStagingHost,
1213
1415
  listAppFiles,
1416
+ logToPlatform,
1214
1417
  manifestIsFresh,
1215
1418
  offerUndo,
1216
1419
  overlaySprigr,
@@ -1235,6 +1438,9 @@ export {
1235
1438
  set,
1236
1439
  tool,
1237
1440
  undoEnvelope,
1441
+ validateLogEntries,
1442
+ validateLogEntry,
1238
1443
  verifyViewerToken,
1239
- withSprigrEmitFallback
1444
+ withSprigrEmitFallback,
1445
+ withSprigrLogFallback
1240
1446
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprigr/apps-app-sdk",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",