@adcp/sdk 15.5.0 → 15.7.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.
Files changed (81) hide show
  1. package/README.md +15 -0
  2. package/dist/lib/auth/oauth/index.d.mts +1 -0
  3. package/dist/lib/auth/oauth/index.d.ts +1 -0
  4. package/dist/lib/auth/oauth/index.js +5 -0
  5. package/dist/lib/auth/oauth/index.mjs +3 -0
  6. package/dist/lib/auth/oauth/pending-web-flow.d.mts +16 -0
  7. package/dist/lib/auth/oauth/pending-web-flow.d.ts +16 -0
  8. package/dist/lib/auth/oauth/pending-web-flow.js +66 -0
  9. package/dist/lib/auth/oauth/pending-web-flow.mjs +41 -0
  10. package/dist/lib/auth/oauth/web-flow.js +3 -3
  11. package/dist/lib/auth/oauth/web-flow.mjs +3 -3
  12. package/dist/lib/registry/types.generated.d.mts +63 -1
  13. package/dist/lib/registry/types.generated.d.ts +63 -1
  14. package/dist/lib/reporting/azure/index.d.mts +26 -0
  15. package/dist/lib/reporting/azure/index.d.ts +26 -0
  16. package/dist/lib/reporting/azure/index.js +247 -0
  17. package/dist/lib/reporting/azure/index.mjs +230 -0
  18. package/dist/lib/reporting/bigquery/index.d.mts +127 -0
  19. package/dist/lib/reporting/bigquery/index.d.ts +127 -0
  20. package/dist/lib/reporting/bigquery/index.js +379 -0
  21. package/dist/lib/reporting/bigquery/index.mjs +341 -0
  22. package/dist/lib/reporting/ledger/handler.js +20 -8
  23. package/dist/lib/reporting/ledger/handler.mjs +20 -8
  24. package/dist/lib/reporting/ledger/postgres.d.mts +21 -0
  25. package/dist/lib/reporting/ledger/postgres.d.ts +21 -0
  26. package/dist/lib/reporting/ledger/postgres.js +35 -2
  27. package/dist/lib/reporting/ledger/postgres.mjs +35 -2
  28. package/dist/lib/reporting/ledger/row-storage-object.js +31 -8
  29. package/dist/lib/reporting/ledger/row-storage-object.mjs +32 -9
  30. package/dist/lib/reporting/ledger/row-storage-provider-io.d.mts +0 -7
  31. package/dist/lib/reporting/ledger/row-storage-provider-io.d.ts +0 -7
  32. package/dist/lib/reporting/ledger/row-storage-provider-io.js +0 -10
  33. package/dist/lib/reporting/ledger/row-storage-provider-io.mjs +0 -8
  34. package/dist/lib/reporting/s3/index.d.mts +29 -0
  35. package/dist/lib/reporting/s3/index.d.ts +29 -0
  36. package/dist/lib/reporting/s3/index.js +324 -0
  37. package/dist/lib/reporting/s3/index.mjs +317 -0
  38. package/dist/lib/reporting/service/index.d.mts +39 -22
  39. package/dist/lib/reporting/service/index.d.ts +39 -22
  40. package/dist/lib/reporting/service/index.js +75 -64
  41. package/dist/lib/reporting/service/index.mjs +79 -63
  42. package/dist/lib/reporting/service/maintenance.d.mts +91 -0
  43. package/dist/lib/reporting/service/maintenance.d.ts +91 -0
  44. package/dist/lib/reporting/service/maintenance.js +140 -0
  45. package/dist/lib/reporting/service/maintenance.mjs +114 -0
  46. package/dist/lib/schemas/browser.d.mts +14 -0
  47. package/dist/lib/schemas/browser.d.ts +14 -0
  48. package/dist/lib/schemas/browser.js +48 -0
  49. package/dist/lib/schemas/browser.mjs +20 -0
  50. package/dist/lib/schemas/index.d.mts +2 -120
  51. package/dist/lib/schemas/index.d.ts +2 -120
  52. package/dist/lib/schemas/index.js +3 -11
  53. package/dist/lib/schemas/index.mjs +1 -9
  54. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  55. package/dist/lib/testing/index.d.mts +1 -0
  56. package/dist/lib/testing/index.d.ts +1 -0
  57. package/dist/lib/testing/index.js +3 -0
  58. package/dist/lib/testing/index.mjs +2 -0
  59. package/dist/lib/testing/pending-web-flow-store.d.mts +20 -0
  60. package/dist/lib/testing/pending-web-flow-store.d.ts +20 -0
  61. package/dist/lib/testing/pending-web-flow-store.js +132 -0
  62. package/dist/lib/testing/pending-web-flow-store.mjs +98 -0
  63. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  64. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  65. package/dist/lib/validation/get-products-request.d.mts +122 -0
  66. package/dist/lib/validation/get-products-request.d.ts +122 -0
  67. package/dist/lib/validation/get-products-request.js +39 -0
  68. package/dist/lib/validation/get-products-request.mjs +14 -0
  69. package/dist/lib/version.d.mts +3 -3
  70. package/dist/lib/version.d.ts +3 -3
  71. package/dist/lib/version.js +3 -3
  72. package/dist/lib/version.mjs +3 -3
  73. package/docs/TYPE-SUMMARY.md +1 -1
  74. package/docs/ZOD-SCHEMAS.md +13 -1
  75. package/docs/guides/OAUTH-ISSUER-BINDING.md +6 -0
  76. package/docs/guides/REPORTING-LEDGER.md +254 -0
  77. package/docs/guides/REPORTING-OPERATIONS.md +82 -1
  78. package/docs/guides/WEB-OAUTH.md +48 -1
  79. package/docs/llms.txt +1 -1
  80. package/docs/migration-14.x-rc-worksheet.md +6 -4
  81. package/package.json +64 -1
@@ -307,6 +307,11 @@ await store.readyRowStorage();
307
307
  - Missing or corrupt rows inside the retention window make
308
308
  `get_media_buy_delivery` fail with `SERVICE_UNAVAILABLE` rather than reporting
309
309
  the revision as absent; expired rows read like an unknown revision.
310
+ - An exact `get_media_buy_delivery` read of an unknown, unauthorized, expired or
311
+ compacted revision, or with a tampered `pagination.cursor`, returns the same
312
+ nondisclosing `REFERENCE_NOT_FOUND` with `field: "reporting_revision_id"`
313
+ (not transient, so buyers do not retry), so a response never reveals whether a
314
+ revision exists for another account.
310
315
  - Managed Delivery claims hydrate PostgreSQL-stored rows automatically.
311
316
 
312
317
  To move existing inline rows into row storage, run the resumable migration in
@@ -376,6 +381,14 @@ await store.readyRowStorage();
376
381
  - Every read verifies the stored bytes against the recorded physical digest
377
382
  before decompressing, then verifies segments, chunks and the revision binding.
378
383
  - Keep a retired binding configured while revisions still reference it.
384
+ - The filesystem provider identifies each write by inode, birth time, mtime
385
+ and size, stamping a random sub-millisecond mtime on every write. It needs a
386
+ filesystem with sub-second mtime and, ideally, birth time (ext4, XFS, APFS,
387
+ NTFS). On a coarse filesystem (FAT, some NFS mounts, ext3), a reused inode
388
+ re-created with identical bytes in the same second gets the same version,
389
+ so a delayed delete could remove the replacement. The conformance check
390
+ `write-unique-version` fails on such filesystems. That failure means the
391
+ filesystem is wrong, not the code.
379
392
  - Custom providers implement `ReportingRowObjectProviderV1`. Run
380
393
  `runReportingRowObjectProviderConformanceV1(provider, { location, prefix })`
381
394
  against a dedicated test prefix before using one.
@@ -417,6 +430,86 @@ const gcs = createGcsReportingRowObjectProviderV1({
417
430
  - The client needs `storage.buckets.get` for the probe plus object create, get
418
431
  and delete on the bucket.
419
432
 
433
+ **Amazon S3 and S3-compatible stores** (`@adcp/sdk/reporting/s3`, optional peer
434
+ `@aws-sdk/client-s3`):
435
+
436
+ ```ts
437
+ import { S3Client } from '@aws-sdk/client-s3';
438
+ import { createS3ReportingRowObjectProviderV1 } from '@adcp/sdk/reporting/s3';
439
+
440
+ const s3 = createS3ReportingRowObjectProviderV1({
441
+ client: new S3Client({ region: 'us-east-1' }), // default credential chain
442
+ });
443
+ // binding: { kind: 'object', provider: 's3', location: { bucket: 'acme-adcp-rows', region: 'us-east-1' }, prefix: 'adcp-rows' }
444
+ // providers: { s3 }
445
+ ```
446
+
447
+ - Endpoint, region, credentials and path style come only from the client you
448
+ inject; `location` accepts just `bucket` and an optional `region`, which the
449
+ probe checks against the client.
450
+ - **Bucket versioning is required.** Writes use `PutObject` with
451
+ `If-None-Match: *` (a 409 is retried once). The recorded native version is
452
+ the write-unique `VersionId`; reads pin it with `VersionId`, and deletes
453
+ confirm the exact version with `HeadObject`, then delete by `VersionId`.
454
+ Deletes must be fenced to one write, and an ETag identifies content, not a
455
+ write: a delayed delete (for example an intent sweep removing an abandoned
456
+ upload) pinned to an ETag would also remove a replacement object later
457
+ re-created with identical bytes at the same key. The probe therefore refuses
458
+ any bucket whose `GetBucketVersioning` status is not `Enabled` (suspended or
459
+ unversioned), and `putIfAbsent` refuses a result with no `VersionId`.
460
+ Deleting a specific `VersionId` permanently removes that version without
461
+ leaving a delete marker, so retention still frees storage; noncurrent-version
462
+ lifecycle rules are not needed for rows this provider deletes.
463
+ - S3-compatible stores may ignore `If-None-Match`, so the probe writes a probe
464
+ key under the binding prefix twice and requires the second write to fail with
465
+ 412, then deletes it. It also refuses missing or inaccessible buckets, a
466
+ bucket policy reported as public, and enabled lifecycle `Expiration` rules
467
+ that can match the prefix. The probe cannot verify Block Public Access on
468
+ every store, so keep row buckets private.
469
+ - The client needs `s3:ListBucket` (HeadBucket),
470
+ `s3:GetLifecycleConfiguration`, `s3:GetBucketVersioning`, object put/get,
471
+ `s3:GetObjectVersion` and `s3:DeleteObjectVersion`.
472
+ `s3:GetBucketPolicyStatus` is optional. For stores without flexible
473
+ checksums, construct the client with `requestChecksumCalculation: 'WHEN_REQUIRED'`.
474
+
475
+ **Azure Blob Storage** (`@adcp/sdk/reporting/azure`, optional peer
476
+ `@azure/storage-blob`):
477
+
478
+ ```ts
479
+ import { DefaultAzureCredential } from '@azure/identity';
480
+ import { BlobServiceClient } from '@azure/storage-blob';
481
+ import { createAzureBlobReportingRowObjectProviderV1 } from '@adcp/sdk/reporting/azure';
482
+
483
+ const azure = createAzureBlobReportingRowObjectProviderV1({
484
+ client: new BlobServiceClient('https://acmerows.blob.core.windows.net', new DefaultAzureCredential()),
485
+ });
486
+ // binding: { kind: 'object', provider: 'azure_blob', location: { account: 'acmerows', container: 'adcp-rows' }, prefix: 'adcp-rows' }
487
+ // providers: { azure_blob: azure }
488
+ ```
489
+
490
+ - `location` names the storage `account` and `container`; the selected client
491
+ must belong to that account. The endpoint and credentials come only from
492
+ the client.
493
+ - **Blob versioning is required** on the storage account. Writes are
494
+ block-blob uploads with `If-None-Match: *`. A 409 `BlobAlreadyExists` or 412
495
+ adopts the existing blob. The recorded native version is the write-unique
496
+ `versionId`; reads and deletes pin it, and only count a response that echoes
497
+ that exact version. ETags are content-derived, so they cannot fence a delayed
498
+ delete away from an identical re-upload at the same key. The probe's
499
+ create-only write must return a `versionId` (otherwise `UNSAFE_BINDING`), and
500
+ `putIfAbsent` refuses a result without one. Deleting a specific version
501
+ permanently removes it, so retention still frees storage. Azurite does not
502
+ implement blob versioning and cannot back this provider. Metadata names are
503
+ stored as C# identifiers (`adcp_intent`).
504
+ - The probe refuses missing or inaccessible containers and containers with
505
+ anonymous access. Like S3, it proves create-only writes empirically with a
506
+ probe blob under the prefix. Lifecycle management policies are account-level
507
+ and are not visible to the data-plane client, so make sure none can delete
508
+ blobs under the binding prefix.
509
+ - The client needs read, write and delete on blobs in the container (for
510
+ example `Storage Blob Data Contributor`), plus permission to delete
511
+ versions.
512
+
420
513
  ### Retention
421
514
 
422
515
  Retention is period-aligned and opt-in. A period becomes eligible once both its
@@ -425,6 +518,17 @@ recordRetentionDays)`, no worker holds its lease, and no Managed Delivery
425
518
  materialization still references it. Superseded snapshots stay readable for the
426
519
  whole window, as the protocol requires.
427
520
 
521
+ When the change feed is installed, retention also waits for registered feed
522
+ consumers. A period is held while any live consumer (one whose cursor was saved
523
+ within `maxFeedHoldDays`, default 7) has not yet passed that period's change
524
+ rows, in the consumer's own feed order. This is the same rule `pruneChanges`
525
+ uses, so a warehouse sink that is briefly down does not lose revisions it has
526
+ not loaded. A held period is simply not retired on this pass. A consumer silent
527
+ for longer than `maxFeedHoldDays` stops holding, so a dead consumer cannot block
528
+ retention forever; it has then fallen behind retention (see the warehouse sink
529
+ below). Pass `maxFeedHoldDays` to `retireExpiredPeriods`; the production service
530
+ passes `changeFeed.maxFeedHoldDays` for you so retention and feed pruning agree.
531
+
428
532
  ```ts
429
533
  await store.retireExpiredPeriods({ statusRetentionDays: 30, recordRetentionDays: 45, limit: 100 });
430
534
  await store.sweepRowWriteIntents({ limit: 100 }); // abandoned object uploads
@@ -442,6 +546,90 @@ because the retired obligations no longer exist.
442
546
  always sweeps abandoned row uploads and expired cursor snapshots and
443
547
  checkpoints, and retires periods when retention is enabled.
444
548
 
549
+ ### Core-only setup with row storage, change feed and retention
550
+
551
+ You do not need the production service (managed delivery, notifications and
552
+ activity) to keep a ledger tidy. Build the store with the options you want and
553
+ give `createReliableReportingService` a `maintenance` option. The production
554
+ service does the same work automatically; the two share one implementation, so
555
+ partial-failure reporting, the `maxFeedHoldDays` that retention honours, per-task
556
+ limits and abort handling are identical.
557
+
558
+ ```ts
559
+ import {
560
+ PostgresReportingLedgerStore,
561
+ REPORTING_LEDGER_MIGRATION,
562
+ REPORTING_ROW_STORAGE_MIGRATION,
563
+ REPORTING_LEDGER_CHANGES_MIGRATION,
564
+ } from '@adcp/sdk/reporting/ledger';
565
+ import { createReliableReportingService } from '@adcp/sdk/reporting/service';
566
+
567
+ // Apply the three migrations, then:
568
+ const store = new PostgresReportingLedgerStore(pool, {
569
+ acknowledgeIsolatedDatabase: true,
570
+ rowStorage, // verified chunked rows in your object store
571
+ changeFeed: true, // host reads and consumer cursors
572
+ });
573
+ await store.readyRowStorage();
574
+
575
+ const reporting = createReliableReportingService({
576
+ store,
577
+ statusRetentionDays: 90, // the commitment advertised to buyers; retention uses it
578
+ // ...adapters, contact, resolveSource, resolveCurrency, resolveCoverage
579
+ maintenance: {
580
+ intervalMilliseconds: 300_000, // default: at most one pass per 5 minutes
581
+ retention: { enabled: true, recordRetentionDays: 120, limit: 100 },
582
+ changeFeed: { changeRetentionDays: 30, maxFeedHoldDays: 7 },
583
+ },
584
+ });
585
+
586
+ reporting.start({ intervalMilliseconds: 60_000, deploymentWide: true, onError });
587
+ ```
588
+
589
+ `reporting.start()` runs maintenance on its first pass and then at most once per
590
+ `maintenance.intervalMilliseconds`, after the tenant cycles, never on every
591
+ cycle. A task that throws, or that resolves with `failed` items, goes to the
592
+ scheduler's `onError` (or `logger.warn`) and never stops the loop; partial
593
+ failures arrive as `ReportingMaintenancePartialFailureError`.
594
+
595
+ If you run your own scheduler instead, call `reporting.runMaintenance({ signal })`.
596
+ It returns one result per task and reports failures in the result rather than
597
+ throwing:
598
+
599
+ ```ts
600
+ for (const result of await reporting.runMaintenance()) {
601
+ if (result.status === 'failed') log.error(result.task, result.error);
602
+ else if (result.partialFailure) log.warn(result.partialFailure.message);
603
+ }
604
+ ```
605
+
606
+ Tasks, in order:
607
+
608
+ | Task | Runs | What it does |
609
+ | --------------------- | ------------------------------------------------- | -------------------------------------------------------------------- |
610
+ | `snapshot sweep` | always | Deletes expired cursor snapshots and checkpoints (1,000 per pass). |
611
+ | `row upload sweep` | always | Resolves abandoned object uploads (100 per pass); no-op without row storage. |
612
+ | `change-feed pruning` | when `maintenance.changeFeed` is set | `pruneChanges` with `changeRetentionDays` and `maxFeedHoldDays`. |
613
+ | `retention` | when `maintenance.retention.enabled` is `true` | `retireExpiredPeriods` with the service's `statusRetentionDays` and the feed's `maxFeedHoldDays`. |
614
+
615
+ Notes:
616
+
617
+ - `maintenance` requires a `PostgresReportingLedgerStore` (or a store with the
618
+ same `retireExpiredPeriods`, `pruneChanges` and `sweepRowWriteIntents`
619
+ methods) and fails at construction otherwise. `maintenance.changeFeed` also
620
+ requires `changeFeed: true` on the store, since pruning is otherwise a silent
621
+ no-op.
622
+ - Retention takes `statusRetentionDays` from the service, so it can never retire
623
+ a period earlier than the retention you advertise. `recordRetentionDays` may
624
+ only lengthen it.
625
+ - The snapshot sweep uses `store.sweepExpiredState()`. A custom store without
626
+ that method skips the task rather than failing.
627
+ - Run it on every replica or on one: each task is bounded, resumable and safe
628
+ to run concurrently.
629
+ - Enable `retention` last (see the adoption checklist in the
630
+ [operations guide](REPORTING-OPERATIONS.md#adopting-row-storage-retention-and-the-change-feed)).
631
+ Retirement cannot be undone.
632
+
445
633
  ### Host reads: current revisions and the change feed
446
634
 
447
635
  In-process consumers such as pacing, alerts and warehouse loaders read from the
@@ -478,9 +666,75 @@ await store.saveFeedConsumerCursor('pacing', page.cursor); // holds back pruning
478
666
  that every consumer updated within `maxFeedHoldDays` (default 7) has passed.
479
667
  An older cursor throws `ReportingChangeCursorExpiredError`; resynchronize from
480
668
  `listCurrentRevisions`.
669
+ - Period retention honours the same hold: `retireExpiredPeriods` does not retire
670
+ a period while a live consumer has not read its changes. A consumer that
671
+ stays silent past `maxFeedHoldDays` stops holding, and its unread changes can
672
+ then point at revisions whose period was retired.
481
673
  - The production service accepts `changeFeed: true | { changeRetentionDays,
482
674
  maxFeedHoldDays }` and prunes the feed on its scheduler.
483
675
 
676
+ ### BigQuery warehouse sink
677
+
678
+ `@adcp/sdk/reporting/bigquery` loads committed revisions into your own BigQuery
679
+ tables for analysts. The warehouse copy is derived and non-authoritative. It
680
+ is never read for serving or verification, so its table names, columns,
681
+ partitioning and retention are yours.
682
+
683
+ ```ts
684
+ import { BigQuery } from '@google-cloud/bigquery';
685
+ import {
686
+ REPORTING_WAREHOUSE_SINK_MIGRATION,
687
+ createBigQueryReportingWarehouseSinkV1,
688
+ } from '@adcp/sdk/reporting/bigquery';
689
+
690
+ await pool.query(REPORTING_WAREHOUSE_SINK_MIGRATION); // after REPORTING_LEDGER_CHANGES_MIGRATION
691
+ const sink = createBigQueryReportingWarehouseSinkV1({
692
+ store, // PostgresReportingLedgerStore with changeFeed: true
693
+ db: pool,
694
+ bigquery: new BigQuery({ projectId: 'analytics-prod' }), // ADC / workload identity
695
+ name: 'warehouse',
696
+ projectId: 'analytics-prod',
697
+ datasetId: 'reporting',
698
+ rowsTableId: 'media_buy_delivery_rows',
699
+ revisionsTableId: 'media_buy_delivery_revisions',
700
+ location: 'US',
701
+ mapRow: row => ({ media_buy_id: row.media_buy_id, impressions: row.impressions, spend: row.spend }),
702
+ labels: { component: 'adcp-reporting' },
703
+ });
704
+ await bigquery.query({ query: sink.defaultTablesSql }); // or create your own tables
705
+ await bigquery.query({ query: sink.currentRowsViewSql });
706
+ setInterval(() => void sink.runOnce(), 15 * 60_000);
707
+ ```
708
+
709
+ - Each run follows the change feed and reads every revision's rows through the
710
+ store's verified reader. It loads rows first and revision metadata second,
711
+ in one load job per table per tick. The sink always adds
712
+ `reporting_revision_id`, `reporting_obligation_id`, `account_id`,
713
+ `revision_number`, `finality`, `period_date` (the period's date in its source
714
+ timezone) and `ordinal` to your mapped columns.
715
+ - Batches are planned durably before loading, and job IDs derive from the
716
+ change-feed range. A crash between a load and the cursor commit replays the
717
+ same batch under the same job ID, which BigQuery refuses to run twice. A job
718
+ that finished with an error is retried under a new attempt for that table
719
+ only.
720
+ - Query `current_rows`, not the raw rows table. Pacing snapshots are
721
+ cumulative, so summing across revisions double-counts. The view keeps each
722
+ obligation's current revision.
723
+ - The sink registers as a change-feed consumer, so `pruneChanges` keeps changes
724
+ it has not loaded yet, and `retireExpiredPeriods` keeps the periods it has not
725
+ loaded yet, in both cases only while its cursor was saved within
726
+ `maxFeedHoldDays`. Superseded snapshots are the intraday pacing curve, and the
727
+ warehouse may keep them as long as you like.
728
+ - `runOnce()` resolves with `{ idle, revisions, rows, missing }`. `missing`
729
+ lists revision IDs the feed announced that were retired before the batch ran.
730
+ A non-empty `missing` means the sink fell behind retention (it was down longer
731
+ than `maxFeedHoldDays` plus the retention window): the warehouse lacks those
732
+ revisions. Backfill them from another source or accept the gap. The cursor
733
+ still advances so the sink is not stuck. After a crash and replay of a planned
734
+ batch, a listed revision may already have been loaded by the first attempt.
735
+ - The sink uses only `dataset().table().load()`, `job().getMetadata()` and
736
+ `query()` from the official client you construct. It adds no dependency.
737
+
484
738
  ## Managed Delivery and Reconciled Billing
485
739
 
486
740
  Core remains the default and has no destination, external-resource, or receipt dependency. To opt into the higher tiers, apply `REPORTING_MANAGED_DELIVERY_MIGRATION` **after** `REPORTING_LEDGER_MIGRATION`, explicitly construct the Core store with `managedDelivery: true`, create a `PostgresReportingManagedDeliveryStore`, and pass both stores with a destination adapter to `createReportingManagedDeliveryRuntime`. The async factory proves the stores share one authority and validates the RC3 tier wiring before returning it. It advertises `managed_delivery` only when an immutable binding, delivery, bounded resource reading, generation-fenced revocation, and at least one verification profile are installed. The advertised automated recovery window must be at least the widest installed managed Core configuration recovery window. It advertises `reconciled_billing` and `receipt_task` only when an authenticated consumer resolver and canonical-digest verification are also installed.
@@ -72,6 +72,71 @@ different operators own tenant partitions. For Reconciled Billing offerings,
72
72
  provide `obligatedConsumers` from a trusted authorization roster. A missing or
73
73
  incomplete roster cannot safely mark a billing obligation reconciled.
74
74
 
75
+ ## Adopting row storage, retention and the change feed
76
+
77
+ Each of these features is opt-in and additive. Enable them one at a time on a
78
+ running deployment. See the [ledger guide](REPORTING-LEDGER.md#row-storage)
79
+ for configuration details.
80
+
81
+ Running Core only, without the production service? Pass `maintenance` to
82
+ `createReliableReportingService` so its scheduler (or `runMaintenance()`) does
83
+ the retention, change-feed pruning and abandoned-upload sweeps the production
84
+ service runs for you. See
85
+ [Core-only setup](REPORTING-LEDGER.md#core-only-setup-with-row-storage-change-feed-and-retention).
86
+
87
+ 1. **Row storage.** Apply `REPORTING_ROW_STORAGE_MIGRATION` (PostgreSQL 13+),
88
+ or pass `rowStorage` to the production service, which adds it to
89
+ `setup.migrations`. Roll out with `rowStorage` set. New revisions store
90
+ verified chunks. Existing revisions keep their inline rows, and every
91
+ binary built with row storage reads both forms. Do not run a writer older
92
+ than this release once chunked revisions exist: it cannot read them.
93
+ 2. **Existing inline rows (optional).** Run `migrateInlineRows` in bounded
94
+ batches from a maintenance job until it returns no cursor. Alert on any
95
+ `quarantined` IDs: those revisions no longer reproduce their committed
96
+ binding and are left untouched for investigation. Then reclaim space with
97
+ `VACUUM FULL` or an online repack in a maintenance window.
98
+ 3. **Object storage.** Create a dedicated private bucket, container or prefix
99
+ for rows, separate from Managed Delivery buckets. Configure the binding in
100
+ host code and run `runReportingRowObjectProviderConformanceV1` against a
101
+ test prefix first. S3 bucket versioning and Azure blob versioning are
102
+ **required**, and the providers' probes refuse unversioned buckets and
103
+ accounts: deletes must be fenced to one write, and ETags are content-derived,
104
+ so an ETag-pinned delayed delete (for example an intent sweep) could remove a
105
+ replacement object re-created with identical bytes at the same key. Deleting
106
+ a specific `VersionId` permanently removes that version (no delete marker),
107
+ so retention still frees storage. Do not
108
+ attach lifecycle rules that can delete live rows sooner than
109
+ `max(statusRetentionDays, recordRetentionDays)` plus the restatement window.
110
+ The GCS probe refuses them; for S3 and Azure, policies outside the client's
111
+ view are the operator's responsibility.
112
+ 4. **Change feed.** Apply `REPORTING_LEDGER_CHANGES_MIGRATION` (PostgreSQL 13+)
113
+ or pass `changeFeed`. Register each durable consumer with
114
+ `saveFeedConsumerCursor`, and size `maxFeedHoldDays` so one stalled consumer
115
+ cannot hold the table indefinitely.
116
+ 5. **Warehouse sink.** Apply `REPORTING_WAREHOUSE_SINK_MIGRATION`, create the
117
+ tables, and the `current_rows` view, then schedule `runOnce`. Point analysts
118
+ at `current_rows`.
119
+ 6. **Retention.** Enable `retention` last, after confirming the advertised
120
+ `status_retention_days` is the window you intend to keep. Retirement deletes
121
+ rows and ledger records for whole periods and cannot be undone. Keep backups
122
+ for any longer audit requirement.
123
+
124
+ Alert on these row-storage signals:
125
+
126
+ - `ROWS_INTEGRITY_FAILED` or `ROWS_UNAVAILABLE` from reads. These are never
127
+ normal, and buyers see `SERVICE_UNAVAILABLE` for the affected revision.
128
+ - A growing count of `adcp_reporting_row_write_intents` in `sweeping` state,
129
+ or `sweepRowWriteIntents` reporting failures. Provider deletes are failing.
130
+ - `retireExpiredPeriods` returning `failed` IDs. Usually a binding was removed
131
+ from configuration while revisions still reference it; keep retired bindings
132
+ configured.
133
+ - `ReportingChangeCursorExpiredError` in a consumer. That consumer fell behind
134
+ retention and must resynchronize from `listCurrentRevisions`.
135
+ - A non-empty `missing` from the warehouse sink's `runOnce()`. The sink was down
136
+ longer than `maxFeedHoldDays`, retention stopped waiting for it, and the
137
+ warehouse lacks those revisions. Backfill them or accept the gap, and keep
138
+ `maxFeedHoldDays` above your longest tolerated sink outage.
139
+
75
140
  ## Service objectives and alerts
76
141
 
77
142
  Choose targets stricter than contractual delivery SLAs. A reasonable starting
@@ -146,7 +211,17 @@ Quarterly, restore into an isolated environment and verify:
146
211
  - notification recovery resumes without duplicate logical events;
147
212
  - buyer cursors/checkpoints resume and duplicate notification keys remain deduped;
148
213
  - a new lease fences an expired owner;
149
- - sampled manifest and canonical-content digests still verify.
214
+ - sampled manifest and canonical-content digests still verify;
215
+ - sampled chunked revisions read back through `getRevision`, including rows in
216
+ object-storage bindings.
217
+
218
+ Row objects live outside the database. A database restore and the matching
219
+ row buckets must come from the same point in time, or revisions committed
220
+ after the bucket snapshot read as `ROWS_UNAVAILABLE`. A restored replica of
221
+ the same authority keeps its installation identity. An independently writable
222
+ clone, such as staging restored from production, must mint a new
223
+ `adcp_persistence_installation` row and use new bindings before it writes or
224
+ sweeps, so it can never delete production objects.
150
225
 
151
226
  After regional failover, fence the old primary before enabling writers. Ensure
152
227
  database time is healthy, then start canary workers and watch lease generation,
@@ -177,6 +252,12 @@ Disable notification-triggered acceleration if necessary, retain dedupe rows,
177
252
  and repair the authenticated seller/principal registry before resuming. Never
178
253
  choose an account based only on an untrusted webhook body.
179
254
 
255
+ Row-storage integrity failure: stop serving the affected scope and do not
256
+ re-upload over the recorded object. Compare the stored object's bytes and
257
+ version with the chunk manifest, restore the exact recorded version from bucket
258
+ versioning or backups, and verify by reading the revision through `getRevision`.
259
+ Never edit a chunk manifest or digest to match the stored bytes.
260
+
180
261
  Clock skew: remove the host from service. Lease and event times use PostgreSQL
181
262
  where correctness needs a shared clock, while provider deadlines and process
182
263
  timeouts still depend on healthy host clocks.
@@ -69,10 +69,57 @@ Auth0 `audience`. With storage, overrides persist/reuse on omission; successful
69
69
  `PendingWebFlowStore`: `put(flow)` and atomic `consume(state)`, e.g. PostgreSQL
70
70
  `DELETE ... WHERE state = $1 AND expires_at > now() RETURNING payload`, or Redis
71
71
  `SET pending:flow:<state> <payload> EX 600 NX` then `GETDEL`. SELECT/DELETE races
72
- permit replay. Store contract tests: `test/lib/oauth-web-flow.test.js`.
72
+ permit replay. Check your store with `assertPendingWebFlowStoreRoundTrip` (below).
73
73
  `DEFAULT_WEB_FLOW_TTL_MS` is 10 minutes; shorten as needed. `InMemoryPendingFlowStore` is
74
74
  test/dev-only; restarts lose flows. Encrypt PKCE verifiers at rest across trust boundaries.
75
75
 
76
+ Use `serializePendingWebFlow` and `parsePendingWebFlow` from `@adcp/sdk/auth`
77
+ for the stored payload. The parser validates SDK-owned fields, revives `createdAt`
78
+ and `expiresAt` as `Date` values, and returns `null` for malformed input. It accepts
79
+ JSON strings and decoded JSON values such as `pg`'s `jsonb` payloads. It preserves
80
+ unknown fields and all client metadata, including `issuer` and `client_secret`.
81
+ Do not narrow the persisted shape to today's fields: a hand-written schema that
82
+ strips new optional fields can type-check while breaking sign-in after an SDK upgrade.
83
+
84
+ ```ts
85
+ import { parsePendingWebFlow, serializePendingWebFlow } from '@adcp/sdk/auth';
86
+ import { assertPendingWebFlowStoreRoundTrip } from '@adcp/sdk/testing';
87
+
88
+ // In put(flow), serialize before encrypting and inserting with duplicate rejection:
89
+ const payload = serializePendingWebFlow(flow);
90
+
91
+ // In consume(state), decrypt the payload returned by atomic get-and-delete:
92
+ function restoreConsumedFlow(state: string, decryptedPayload: unknown) {
93
+ const restored = parsePendingWebFlow(decryptedPayload);
94
+ return restored && restored.state === state && restored.expiresAt.getTime() > Date.now()
95
+ ? restored
96
+ : null;
97
+ }
98
+
99
+ // In CI, use a disposable instance of your production store on each SDK upgrade:
100
+ await assertPendingWebFlowStoreRoundTrip(pendingFlowStore);
101
+ // Stores with application-owned constraints can supply valid fixture values:
102
+ await assertPendingWebFlowStoreRoundTrip(pendingFlowStore, {
103
+ agentId: testAgentId,
104
+ carry: { user_id: testUserId, nested: { values: ['one', 2, null] } },
105
+ });
106
+ ```
107
+
108
+ The helper checks every current SDK field (including nested `carry` and issuer-bound
109
+ client secrets), Date revival, missing states, single use, expired rows, duplicate
110
+ rejection, and concurrent consume. It also checks null/absent snapshots, clear actions,
111
+ public clients, registration metadata, and unknown extensions. Stores may reject
112
+ already-expired inserts; the helper still checks that consumption returns `null`.
113
+ Its fixture is exhaustive at SDK build time, including optional flow and client
114
+ information fields. The concurrency check is a smoke test; the backend must
115
+ provide atomic deletion across processes. Use JSON-serializable `carry` and validate
116
+ its application-owned contents separately. Keep absent optional fields absent when
117
+ hydrating rows; an undefined-valued own `resourceOverrideSnapshot` can trigger the
118
+ callback's concurrent-edit guard. The store still owns encryption of PKCE
119
+ and client secrets, TTL, duplicate rejection, atomic consume, and binding the payload's
120
+ `state` to its storage key. The parser does not filter expired flows or establish
121
+ issuer trust; `completeWebOAuthFlow` also enforces expiry and issuer binding.
122
+
76
123
  Optional `agentStorage` (`OAuthConfigStorage`): callback loads the pending row's
77
124
  agent ID. Without it, completion
78
125
  returns `persisted: false` for application persistence. Preserve the issuer guide's
package/docs/llms.txt CHANGED
@@ -1,7 +1,7 @@
1
1
  # Ad Context Protocol (AdCP)
2
2
 
3
3
  > Generated at: 2026-10-09
4
- > Library: @adcp/sdk v15.5.0
4
+ > Library: @adcp/sdk v15.7.0
5
5
  > AdCP major version: 3
6
6
  > Canonical URL: https://adcontextprotocol.github.io/adcp-client/llms.txt
7
7
  > Note: the `Library` stamp reflects the package.json version at doc-generation time. The narrative below describes the surface that lands on the next-published minor — including any 6.7 helpers documented here ahead of the release tag.
@@ -11,8 +11,8 @@ below as the publication/deployment gate.
11
11
 
12
12
  | Fact | Value |
13
13
  | --- | --- |
14
- | Exact npm package | `@adcp/sdk@15.5.0` |
15
- | npm integrity | registry-derived after publication; run `npm view @adcp/sdk@15.5.0 dist.integrity` |
14
+ | Exact npm package | `@adcp/sdk@15.7.0` |
15
+ | npm integrity | registry-derived after publication; run `npm view @adcp/sdk@15.7.0 dist.integrity` |
16
16
  | Node.js runtime | `^20.19.0 || >=22.12.0` |
17
17
  | Default AdCP wire release | `3.2.3` |
18
18
  | Maintained wire releases | `v2.5`, `v2.6`, `v3`, `3.0.0`, `3.0`, `3.0.1`, `3.0.2`, `3.0.3`, `3.0.4`, `3.0.5`, `3.0.6`, `3.0.7`, `3.0.8`, `3.0.9`, `3.0.10`, `3.0.11`, `3.0.12`, `3.0.13`, `3.0.14`, `3.0.15`, `3.0.16`, `3.0.17`, `3.0.18`, `3.0.19`, `3.0.20`, `3.0.21`, `3.0.22`, `3.0.23`, `3.0.24`, `3.0.25`, `3.1.0`, `3.1`, `3.1.1`, `3.1.2`, `3.1.3`, `3.1.4`, `3.1.5`, `3.1.6`, `3.1.7`, `3.1.8`, `3.1.9`, `3.1.10`, `3.1.11`, `3.1.12`, `3.1.13`, `3.1.14`, `3.1.15`, `3.1.16`, `3.1.17`, `3.1.18`, `3.1.19`, `3.1.20`, `3.1.21`, `3.1.22`, `3.1.23`, `3.1.24`, `3.2.1`, `3.2`, `3.2.2`, `3.2.3` |
@@ -21,8 +21,8 @@ below as the publication/deployment gate.
21
21
  Install exact production inputs rather than a moving range or dist-tag:
22
22
 
23
23
  ```bash
24
- npm install --save-exact '@adcp/sdk@15.5.0'
25
- npm view '@adcp/sdk@15.5.0' dist.integrity
24
+ npm install --save-exact '@adcp/sdk@15.7.0'
25
+ npm view '@adcp/sdk@15.7.0' dist.integrity
26
26
  ```
27
27
 
28
28
  ### Required and optional peers
@@ -32,6 +32,8 @@ Install the peer packages used by your application at versions satisfying these
32
32
  | Peer | Supported range | Installation |
33
33
  | --- | --- | --- |
34
34
  | `@a2a-js/sdk` | `^1.0.1` | Required |
35
+ | `@aws-sdk/client-s3` | `^3.700.0` | Optional |
36
+ | `@azure/storage-blob` | `^12.23.0` | Optional |
35
37
  | `@google-cloud/storage` | `^7.22.0 || ^8.2.0` | Optional |
36
38
  | `@modelcontextprotocol/sdk` | `^1.31.0` | Required |
37
39
  | `@opentelemetry/api` | `^1.0.0` | Optional |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adcp/sdk",
3
- "version": "15.5.0",
3
+ "version": "15.7.0",
4
4
  "description": "AdCP SDK — client, server, and compliance harnesses for the AdContext Protocol (MCP + A2A)",
5
5
  "workspaces": [
6
6
  ".",
@@ -284,6 +284,16 @@
284
284
  "default": "./dist/lib/schemas/index.js"
285
285
  }
286
286
  },
287
+ "./schemas/browser": {
288
+ "import": {
289
+ "types": "./dist/lib/schemas/browser.d.mts",
290
+ "default": "./dist/lib/schemas/browser.mjs"
291
+ },
292
+ "require": {
293
+ "types": "./dist/lib/schemas/browser.d.ts",
294
+ "default": "./dist/lib/schemas/browser.js"
295
+ }
296
+ },
287
297
  "./compliance-fixtures": {
288
298
  "import": {
289
299
  "types": "./dist/lib/compliance-fixtures/index.d.mts",
@@ -419,6 +429,36 @@
419
429
  "types": "./dist/lib/reporting/gcs/index.d.ts",
420
430
  "default": "./dist/lib/reporting/gcs/index.js"
421
431
  }
432
+ },
433
+ "./reporting/s3": {
434
+ "import": {
435
+ "types": "./dist/lib/reporting/s3/index.d.mts",
436
+ "default": "./dist/lib/reporting/s3/index.mjs"
437
+ },
438
+ "require": {
439
+ "types": "./dist/lib/reporting/s3/index.d.ts",
440
+ "default": "./dist/lib/reporting/s3/index.js"
441
+ }
442
+ },
443
+ "./reporting/azure": {
444
+ "import": {
445
+ "types": "./dist/lib/reporting/azure/index.d.mts",
446
+ "default": "./dist/lib/reporting/azure/index.mjs"
447
+ },
448
+ "require": {
449
+ "types": "./dist/lib/reporting/azure/index.d.ts",
450
+ "default": "./dist/lib/reporting/azure/index.js"
451
+ }
452
+ },
453
+ "./reporting/bigquery": {
454
+ "import": {
455
+ "types": "./dist/lib/reporting/bigquery/index.d.mts",
456
+ "default": "./dist/lib/reporting/bigquery/index.mjs"
457
+ },
458
+ "require": {
459
+ "types": "./dist/lib/reporting/bigquery/index.d.ts",
460
+ "default": "./dist/lib/reporting/bigquery/index.js"
461
+ }
422
462
  }
423
463
  },
424
464
  "typesVersions": {
@@ -489,6 +529,9 @@
489
529
  "schemas": [
490
530
  "dist/lib/schemas/index.d.ts"
491
531
  ],
532
+ "schemas/browser": [
533
+ "dist/lib/schemas/browser.d.ts"
534
+ ],
492
535
  "compliance-fixtures": [
493
536
  "dist/lib/compliance-fixtures/index.d.ts"
494
537
  ],
@@ -542,6 +585,15 @@
542
585
  ],
543
586
  "reporting/gcs": [
544
587
  "dist/lib/reporting/gcs/index.d.ts"
588
+ ],
589
+ "reporting/s3": [
590
+ "dist/lib/reporting/s3/index.d.ts"
591
+ ],
592
+ "reporting/azure": [
593
+ "dist/lib/reporting/azure/index.d.ts"
594
+ ],
595
+ "reporting/bigquery": [
596
+ "dist/lib/reporting/bigquery/index.d.ts"
545
597
  ]
546
598
  }
547
599
  },
@@ -684,6 +736,7 @@
684
736
  "ci:validate": "node scripts/ci-validate.js",
685
737
  "ci:quick": "npm run format:check && npm run typecheck && npm run build:lib && npm test",
686
738
  "test:zod-openapi-portability": "node --test test/lib/zod-openapi-portability.test.js",
739
+ "test:schemas-browser": "node --test-timeout=60000 --test test/lib/schemas-browser.test.js",
687
740
  "ci:schema-check": "npm run sync-schemas && npm run generate-types && npm run generate-registry-types -- --sync && git diff --exit-code -I '// Generated at:' src/lib/types/ src/lib/agents/ src/lib/media-buy/action-metadata.generated.ts src/lib/media-buy/update-fields.generated.ts src/lib/registry/types.generated.ts schemas/registry/registry.yaml || (echo '⚠️ Generated files are out of sync. Run: npm run sync-schemas && npm run generate-types && npm run generate-registry-types -- --sync' && exit 1)",
688
741
  "ci:codegen-strict": "tsx scripts/check-no-loose-oneof.ts",
689
742
  "ci:docs-check": "npm run generate-agent-docs && npm run generate-release-worksheet && git diff --exit-code -I '> Generated at:' docs/llms.txt docs/TYPE-SUMMARY.md docs/migration-14.x-rc-worksheet.md || (echo '⚠️ Generated docs are out of sync. Run: npm run generate-agent-docs && npm run generate-release-worksheet' && exit 1)",
@@ -746,6 +799,8 @@
746
799
  },
747
800
  "peerDependencies": {
748
801
  "@a2a-js/sdk": "^1.0.1",
802
+ "@aws-sdk/client-s3": "^3.700.0",
803
+ "@azure/storage-blob": "^12.23.0",
749
804
  "@google-cloud/storage": "^7.22.0 || ^8.2.0",
750
805
  "@modelcontextprotocol/sdk": "^1.31.0",
751
806
  "@opentelemetry/api": "^1.0.0",
@@ -753,6 +808,12 @@
753
808
  "zod": "^4.1.5"
754
809
  },
755
810
  "peerDependenciesMeta": {
811
+ "@aws-sdk/client-s3": {
812
+ "optional": true
813
+ },
814
+ "@azure/storage-blob": {
815
+ "optional": true
816
+ },
756
817
  "@google-cloud/storage": {
757
818
  "optional": true
758
819
  },
@@ -767,6 +828,8 @@
767
828
  "@a2a-js/sdk": "^1.0.1",
768
829
  "@apidevtools/json-schema-ref-parser": "^15.5.2",
769
830
  "@arethetypeswrong/cli": "^0.18.5",
831
+ "@aws-sdk/client-s3": "^3.1147.0",
832
+ "@azure/storage-blob": "~12.32.0",
770
833
  "@changesets/cli": "^3.0.3",
771
834
  "@commitlint/cli": "^20.5.3",
772
835
  "@commitlint/config-conventional": "^20.5.3",