@pithy-sh/core 0.10.0 → 0.11.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 (180) hide show
  1. package/dist/access/access.d.ts +172 -0
  2. package/dist/access/access.d.ts.map +1 -0
  3. package/dist/access/access.js +147 -0
  4. package/dist/access/acting.d.ts +49 -0
  5. package/dist/access/acting.d.ts.map +1 -0
  6. package/dist/access/acting.js +21 -0
  7. package/dist/access/policyCheck.d.ts +33 -0
  8. package/dist/access/policyCheck.d.ts.map +1 -0
  9. package/dist/access/policyCheck.js +48 -0
  10. package/dist/access/usage.d.ts +83 -0
  11. package/dist/access/usage.d.ts.map +1 -0
  12. package/dist/access/usage.js +104 -0
  13. package/dist/access/visibility.d.ts +66 -0
  14. package/dist/access/visibility.d.ts.map +1 -0
  15. package/dist/access/visibility.js +86 -0
  16. package/dist/audit/auditEvent.d.ts +5 -0
  17. package/dist/audit/auditEvent.d.ts.map +1 -1
  18. package/dist/audit/auditEvent.js +2 -0
  19. package/dist/capability/bindings.d.ts +47 -2
  20. package/dist/capability/bindings.d.ts.map +1 -1
  21. package/dist/capability/bindings.js +117 -7
  22. package/dist/capability/capability.d.ts +98 -28
  23. package/dist/capability/capability.d.ts.map +1 -1
  24. package/dist/capability/manifest.d.ts +2 -1
  25. package/dist/capability/manifest.d.ts.map +1 -1
  26. package/dist/capability/manifest.js +1 -1
  27. package/dist/capability/settings.d.ts +86 -2
  28. package/dist/capability/settings.d.ts.map +1 -1
  29. package/dist/capability/settings.js +80 -1
  30. package/dist/cli/commandCatalog.generated.d.ts +184 -22
  31. package/dist/cli/commandCatalog.generated.d.ts.map +1 -1
  32. package/dist/cli/commandCatalog.generated.js +196 -22
  33. package/dist/cli/commandFacts.d.ts +1 -1
  34. package/dist/cli/commandFacts.d.ts.map +1 -1
  35. package/dist/cli/commandFacts.js +1 -0
  36. package/dist/controlPlane/capability.d.ts.map +1 -1
  37. package/dist/controlPlane/capability.js +12 -2
  38. package/dist/controlPlane/context.d.ts +5 -0
  39. package/dist/controlPlane/context.d.ts.map +1 -1
  40. package/dist/controlPlane/context.js +3 -1
  41. package/dist/controlPlane/data/connection.d.ts +47 -0
  42. package/dist/controlPlane/data/connection.d.ts.map +1 -1
  43. package/dist/controlPlane/data/connection.js +55 -1
  44. package/dist/controlPlane/http/cors.d.ts +4 -4
  45. package/dist/controlPlane/http/cors.js +4 -4
  46. package/dist/controlPlane/http/guard.d.ts +11 -0
  47. package/dist/controlPlane/http/guard.d.ts.map +1 -1
  48. package/dist/controlPlane/http/guard.js +17 -1
  49. package/dist/controlPlane/http/handlers.d.ts +60 -0
  50. package/dist/controlPlane/http/handlers.d.ts.map +1 -1
  51. package/dist/controlPlane/http/handlers.js +31 -2
  52. package/dist/controlPlane/http/readiness.d.ts +33 -0
  53. package/dist/controlPlane/http/readiness.d.ts.map +1 -0
  54. package/dist/controlPlane/http/readiness.js +39 -0
  55. package/dist/controlPlane/http/responses.d.ts +147 -0
  56. package/dist/controlPlane/http/responses.d.ts.map +1 -1
  57. package/dist/controlPlane/http/responses.js +61 -4
  58. package/dist/controlPlane/http/routes.d.ts +1 -0
  59. package/dist/controlPlane/http/routes.d.ts.map +1 -1
  60. package/dist/controlPlane/http/routes.js +17 -3
  61. package/dist/controlPlane/http/verify.d.ts.map +1 -1
  62. package/dist/controlPlane/http/verify.js +2 -1
  63. package/dist/controlPlane/migrationState/migrationState.d.ts +102 -0
  64. package/dist/controlPlane/migrationState/migrationState.d.ts.map +1 -0
  65. package/dist/controlPlane/migrationState/migrationState.js +142 -0
  66. package/dist/controlPlane/migrations/0003_connection_principal.d.ts +21 -0
  67. package/dist/controlPlane/migrations/0003_connection_principal.d.ts.map +1 -0
  68. package/dist/controlPlane/migrations/0003_connection_principal.js +33 -0
  69. package/dist/controlPlane/scope/scope.d.ts +13 -2
  70. package/dist/controlPlane/scope/scope.d.ts.map +1 -1
  71. package/dist/controlPlane/scope/scope.js +14 -3
  72. package/dist/createBackend.d.ts.map +1 -1
  73. package/dist/createBackend.js +7 -2
  74. package/dist/createEntrypoint.d.ts.map +1 -1
  75. package/dist/createEntrypoint.js +5 -3
  76. package/dist/entitlement/allowance.d.ts +25 -0
  77. package/dist/entitlement/allowance.d.ts.map +1 -0
  78. package/dist/entitlement/allowance.js +80 -0
  79. package/dist/entitlement/entitlement.d.ts +41 -0
  80. package/dist/entitlement/entitlement.d.ts.map +1 -1
  81. package/dist/entitlement/meter.d.ts +57 -0
  82. package/dist/entitlement/meter.d.ts.map +1 -0
  83. package/dist/entitlement/meter.js +13 -0
  84. package/dist/error/http.d.ts +144 -0
  85. package/dist/error/http.d.ts.map +1 -1
  86. package/dist/error/payload.d.ts +288 -0
  87. package/dist/error/payload.d.ts.map +1 -1
  88. package/dist/error/payload.js +100 -4
  89. package/dist/http/authContext.d.ts +7 -0
  90. package/dist/http/authContext.d.ts.map +1 -1
  91. package/dist/http/authContext.js +8 -1
  92. package/dist/http/scopeGates.d.ts +20 -0
  93. package/dist/http/scopeGates.d.ts.map +1 -0
  94. package/dist/http/scopeGates.js +36 -0
  95. package/dist/index.d.ts +1 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +2 -2
  98. package/dist/logger/record.d.ts +84 -0
  99. package/dist/logger/record.d.ts.map +1 -1
  100. package/dist/migrations/capabilitySets.d.ts +23 -0
  101. package/dist/migrations/capabilitySets.d.ts.map +1 -0
  102. package/dist/migrations/capabilitySets.js +74 -0
  103. package/dist/migrations/registry.d.ts +26 -0
  104. package/dist/migrations/registry.d.ts.map +1 -1
  105. package/dist/migrations/registry.js +38 -7
  106. package/dist/migrations/state.d.ts +60 -0
  107. package/dist/migrations/state.d.ts.map +1 -0
  108. package/dist/migrations/state.js +46 -0
  109. package/dist/naming/feature.d.ts +27 -5
  110. package/dist/naming/feature.d.ts.map +1 -1
  111. package/dist/naming/feature.js +36 -6
  112. package/dist/naming/limits.d.ts +47 -3
  113. package/dist/naming/limits.d.ts.map +1 -1
  114. package/dist/naming/limits.js +47 -3
  115. package/dist/naming/provisionScope.d.ts +6 -0
  116. package/dist/naming/provisionScope.d.ts.map +1 -1
  117. package/dist/naming/provisionScope.js +8 -3
  118. package/dist/naming/resourceNames.d.ts +21 -2
  119. package/dist/naming/resourceNames.d.ts.map +1 -1
  120. package/dist/naming/resourceNames.js +21 -3
  121. package/dist/seed/writeD1.d.ts +7 -1
  122. package/dist/seed/writeD1.d.ts.map +1 -1
  123. package/dist/seed/writeD1.js +9 -3
  124. package/dist/seed/writeKv.d.ts +2 -0
  125. package/dist/seed/writeKv.d.ts.map +1 -1
  126. package/dist/seed/writeKv.js +6 -2
  127. package/dist/version.generated.d.ts +1 -1
  128. package/dist/version.generated.js +1 -1
  129. package/dist/worker/serviceLoopback.d.ts +36 -0
  130. package/dist/worker/serviceLoopback.d.ts.map +1 -0
  131. package/dist/worker/serviceLoopback.js +74 -0
  132. package/dist/workflow/faults.d.ts +12 -1
  133. package/dist/workflow/faults.d.ts.map +1 -1
  134. package/dist/workflow/faults.js +22 -15
  135. package/package.json +2 -2
  136. package/src/access/access.ts +298 -0
  137. package/src/access/acting.ts +59 -0
  138. package/src/access/policyCheck.ts +77 -0
  139. package/src/access/usage.ts +141 -0
  140. package/src/access/visibility.ts +109 -0
  141. package/src/audit/auditEvent.ts +4 -0
  142. package/src/capability/bindings.ts +159 -13
  143. package/src/capability/capability.ts +99 -29
  144. package/src/capability/settings.ts +146 -2
  145. package/src/cli/commandCatalog.generated.ts +212 -22
  146. package/src/cli/commandFacts.ts +1 -0
  147. package/src/controlPlane/capability.ts +25 -1
  148. package/src/controlPlane/context.ts +4 -0
  149. package/src/controlPlane/data/connection.ts +90 -0
  150. package/src/controlPlane/http/cors.ts +4 -4
  151. package/src/controlPlane/http/guard.ts +21 -0
  152. package/src/controlPlane/http/handlers.ts +41 -0
  153. package/src/controlPlane/http/readiness.ts +50 -0
  154. package/src/controlPlane/http/responses.ts +140 -2
  155. package/src/controlPlane/http/routes.ts +25 -1
  156. package/src/controlPlane/http/verify.ts +2 -0
  157. package/src/controlPlane/migrationState/migrationState.ts +200 -0
  158. package/src/controlPlane/migrations/0003_connection_principal.ts +37 -0
  159. package/src/controlPlane/scope/scope.ts +14 -2
  160. package/src/createBackend.ts +24 -6
  161. package/src/createEntrypoint.ts +9 -3
  162. package/src/entitlement/allowance.ts +85 -0
  163. package/src/entitlement/entitlement.ts +38 -0
  164. package/src/entitlement/meter.ts +69 -0
  165. package/src/error/payload.ts +211 -4
  166. package/src/http/authContext.ts +13 -1
  167. package/src/http/scopeGates.ts +34 -0
  168. package/src/index.ts +1 -0
  169. package/src/migrations/capabilitySets.ts +87 -0
  170. package/src/migrations/registry.ts +46 -2
  171. package/src/migrations/state.ts +94 -0
  172. package/src/naming/feature.ts +46 -7
  173. package/src/naming/limits.ts +35 -3
  174. package/src/naming/provisionScope.ts +19 -1
  175. package/src/naming/resourceNames.ts +52 -5
  176. package/src/seed/writeD1.ts +13 -4
  177. package/src/seed/writeKv.ts +6 -2
  178. package/src/version.generated.ts +1 -1
  179. package/src/worker/serviceLoopback.ts +105 -0
  180. package/src/workflow/faults.ts +51 -24
@@ -501,6 +501,28 @@ const TurnstileConfigPublic = z
501
501
  })
502
502
  .describe("The Turnstile middleware is misconfigured (500).");
503
503
 
504
+ const TurnstileAnalyticsWindowPublic = z
505
+ .object({
506
+ code: z
507
+ .literal("turnstile/analytics_window")
508
+ .describe(
509
+ "The requested challenge window reaches past what Cloudflare keeps, or is wider than one read may ask for.",
510
+ ),
511
+ status: z.literal(400).describe("Bad Request — narrow the window or move it later."),
512
+ ...publicFields,
513
+ })
514
+ .describe("A Turnstile-analytics window outside Cloudflare's retention or range (400).");
515
+
516
+ const TurnstileAnalyticsUnavailablePublic = z
517
+ .object({
518
+ code: z
519
+ .literal("turnstile/analytics_unavailable")
520
+ .describe("Cloudflare does not serve Turnstile analytics to this account, so no outcome can be read."),
521
+ status: z.literal(409).describe("Conflict — the account's configuration rules the read out."),
522
+ ...publicFields,
523
+ })
524
+ .describe("Turnstile analytics are not served to this account (409).");
525
+
504
526
  // --- @pithy-sh/audit: audit-trail recorder codes ---
505
527
 
506
528
  const AuditInvalidEventPublic = z
@@ -535,6 +557,18 @@ const MediaNotFoundPublic = z
535
557
  })
536
558
  .describe("A requested media record does not exist (404).");
537
559
 
560
+ const MediaForbiddenPublic = z
561
+ .object({
562
+ code: z
563
+ .literal("media/forbidden")
564
+ .describe(
565
+ "The caller may read the media record but not change it: only its owner, acting for the record's organization, or a role holding `media:manage` there may edit, delete or re-scope it.",
566
+ ),
567
+ status: z.literal(403).describe("Forbidden."),
568
+ ...publicFields,
569
+ })
570
+ .describe("The caller may read the record but not change it (403).");
571
+
538
572
  const MediaUnsupportedPublic = z
539
573
  .object({
540
574
  code: z
@@ -565,6 +599,30 @@ const MediaEnrichmentFailedPublic = z
565
599
  })
566
600
  .describe("A media AI-enrichment step failed (500).");
567
601
 
602
+ const MediaAnalyticsWindowPublic = z
603
+ .object({
604
+ code: z
605
+ .literal("media/analytics_window")
606
+ .describe(
607
+ "The requested delivery window reaches past what Cloudflare keeps, or is wider than one read may ask for.",
608
+ ),
609
+ status: z.literal(400).describe("Bad Request — narrow the window or move it later."),
610
+ ...publicFields,
611
+ })
612
+ .describe("A delivery-analytics window outside Cloudflare's retention or range (400).");
613
+
614
+ const MediaAnalyticsUnavailablePublic = z
615
+ .object({
616
+ code: z
617
+ .literal("media/analytics_unavailable")
618
+ .describe(
619
+ "Cloudflare does not serve the delivery analytics this read needs to this account — a Customer Metadata Boundary outside the US carries none of them.",
620
+ ),
621
+ status: z.literal(409).describe("Conflict — the account's configuration rules the read out."),
622
+ ...publicFields,
623
+ })
624
+ .describe("Delivery analytics are not served to this account (409).");
625
+
568
626
  // --- @pithy-sh/leaderboard: optional ranking capability codes ---
569
627
 
570
628
  const LeaderboardBoardNotFoundPublic = z
@@ -931,7 +989,7 @@ const StorageNotFoundPublic = z
931
989
  .object({
932
990
  code: z
933
991
  .literal("storage/not_found")
934
- .describe("No object row matches, or the object exists but is not visible to this caller."),
992
+ .describe("No object row matches, or the object exists but this caller may not read it."),
935
993
  status: z.literal(404).describe("Not Found."),
936
994
  ...publicFields,
937
995
  })
@@ -939,18 +997,22 @@ const StorageNotFoundPublic = z
939
997
 
940
998
  const StorageForbiddenPublic = z
941
999
  .object({
942
- code: z.literal("storage/forbidden").describe("The caller does not own the object and the object is not public."),
1000
+ code: z
1001
+ .literal("storage/forbidden")
1002
+ .describe(
1003
+ "The caller may read the object but not change it: only its owner, acting for the object's organization, or a role holding `storage:manage` there may.",
1004
+ ),
943
1005
  status: z.literal(403).describe("Forbidden."),
944
1006
  ...publicFields,
945
1007
  })
946
- .describe("The caller is not the owner and the object is not public (403).");
1008
+ .describe("The caller may read the object but not change it (403).");
947
1009
 
948
1010
  const StorageQuotaExceededPublic = z
949
1011
  .object({
950
1012
  code: z
951
1013
  .literal("storage/quota_exceeded")
952
1014
  .describe(
953
- "The declared upload size would push the owner past their byte quota. Checked at init against pending and stored rows together, so concurrent inits cannot race past the limit.",
1015
+ "The declared upload size would push the owner past the configured `bytesPerOwner` quota. Checked at init against pending and stored rows together, so concurrent inits cannot race past the limit. Where the `storageBytes` meter is enforced, the same refusal is `payments/quota_exceeded` (402).",
954
1016
  ),
955
1017
  status: z.literal(413).describe("Content Too Large."),
956
1018
  ...publicFields,
@@ -1257,6 +1319,30 @@ const PaymentsSubscriptionChangeRefusedPublic = z
1257
1319
  })
1258
1320
  .describe("A subscription's state forbids the change asked of it (409).");
1259
1321
 
1322
+ const PaymentsOverrideStalePublic = z
1323
+ .object({
1324
+ code: z
1325
+ .literal("payments/override_stale")
1326
+ .describe(
1327
+ "A control-plane override write named a version behind the subject's override row. Two operators share one customer: both opened the same allowance, and the second would silently overwrite the first while the first believed they had succeeded. So the write is refused and nothing is written. 409, because the request is well-formed and the same request against the version it now holds would be accepted — the caller re-reads, and tries again.",
1328
+ ),
1329
+ status: z.literal(409).describe("Conflict — the override changed since the caller read it."),
1330
+ ...publicFields,
1331
+ })
1332
+ .describe("An override write named a stale version (409).");
1333
+
1334
+ const PaymentsQuotaExceededPublic = z
1335
+ .object({
1336
+ code: z
1337
+ .literal("payments/quota_exceeded")
1338
+ .describe(
1339
+ "An enforced kit meter — emails enqueued, bytes stored, media items held — would pass the subject's effective quota for the period. Nothing was written. 402 Payment Required, because it is the honest status: the plan does not allow more and the remedy is a purchase, a bigger tier, or a number pinned for this subject from the control plane. A meter refuses and never bills — the kit does not sell overage.",
1340
+ ),
1341
+ status: z.literal(402).describe("Payment Required — the plan's allowance for this period is used up."),
1342
+ ...publicFields,
1343
+ })
1344
+ .describe("An enforced meter is at its quota for this period (402).");
1345
+
1260
1346
  // --- @pithy-sh/core: the `control-plane` verification strategy ---
1261
1347
  //
1262
1348
  // NOT Cloudflare's control plane. Everywhere else in this repo "control plane" means the Cloudflare
@@ -1407,6 +1493,68 @@ const OrganizationNominationInvalidPublic = z
1407
1493
  })
1408
1494
  .describe("No standing ownership offer this caller may accept (400).");
1409
1495
 
1496
+ // --- @pithy-sh/artifacts: Git repositories, ownership, grants and short-lived tokens ---
1497
+
1498
+ const ArtifactsNotFoundPublic = z
1499
+ .object({
1500
+ code: z
1501
+ .literal("artifacts/not_found")
1502
+ .describe(
1503
+ "No such repository, **or** one the caller holds no grant on and does not own. One code for both, byte for byte: a repository's name is in its clone URL, and a refusal that told the two apart would hand it over. Also what the credential route answers for a repository with remote access off. Which it was survives in `detail`, which the HTTP codec strips.",
1504
+ ),
1505
+ status: z.literal(404).describe("Not Found."),
1506
+ ...publicFields,
1507
+ })
1508
+ .describe("No such repository, or none this caller may see — the same answer for both (404).");
1509
+
1510
+ const ArtifactsForbiddenPublic = z
1511
+ .object({
1512
+ code: z
1513
+ .literal("artifacts/forbidden")
1514
+ .describe(
1515
+ "The caller can see the repository and may not do this to it: a grant holder asking for what only an owner may do, or a member of an organization whose role lacks the configured `managePower`. A 403 rather than the 404 above, because by then the caller already knows the repository exists.",
1516
+ ),
1517
+ status: z.literal(403).describe("Forbidden."),
1518
+ ...publicFields,
1519
+ })
1520
+ .describe("The caller may see this repository and may not do this (403).");
1521
+
1522
+ const ArtifactsInvalidNamePublic = z
1523
+ .object({
1524
+ code: z
1525
+ .literal("artifacts/invalid_name")
1526
+ .describe(
1527
+ "A supplied repository name is outside Cloudflare's charset or the 2-to-63 cap. Refused and never shortened: a name somebody chose is in every clone URL, and a silently different one is a repository nobody can find.",
1528
+ ),
1529
+ status: z.literal(400).describe("Bad Request."),
1530
+ ...publicFields,
1531
+ })
1532
+ .describe("A repository name breaks the naming rule (400).");
1533
+
1534
+ const ArtifactsNameTakenPublic = z
1535
+ .object({
1536
+ code: z
1537
+ .literal("artifacts/name_taken")
1538
+ .describe(
1539
+ "The owner already holds a repository with that display name, or a supplied physical name is already in the namespace. The second happens only where the adopter supplies names, so uniqueness is theirs.",
1540
+ ),
1541
+ status: z.literal(409).describe("Conflict."),
1542
+ ...publicFields,
1543
+ })
1544
+ .describe("That repository name is already in use (409).");
1545
+
1546
+ const ArtifactsInvalidConfigPublic = z
1547
+ .object({
1548
+ code: z
1549
+ .literal("artifacts/invalid_config")
1550
+ .describe(
1551
+ "The capability's configuration cannot be honored: a `tokenTtlSeconds` above the ceiling, or `organization` composed with no `managePower` named. Raised at boot, so a mistake fails on deploy rather than at the first mint.",
1552
+ ),
1553
+ status: z.literal(400).describe("Bad Request — the configuration is malformed."),
1554
+ ...publicFields,
1555
+ })
1556
+ .describe("The artifacts configuration is invalid (400).");
1557
+
1410
1558
  // --- @pithy-sh/support: inbound inbox, taxonomy, and classification codes ---
1411
1559
 
1412
1560
  const SupportNotFoundPublic = z
@@ -1639,12 +1787,17 @@ export const KitPublicErrorPayload = z
1639
1787
  TurnstileMissingTokenPublic,
1640
1788
  TurnstileFailedPublic,
1641
1789
  TurnstileConfigPublic,
1790
+ TurnstileAnalyticsWindowPublic,
1791
+ TurnstileAnalyticsUnavailablePublic,
1642
1792
  AuditInvalidEventPublic,
1643
1793
  AuditWriteFailedPublic,
1644
1794
  MediaNotFoundPublic,
1795
+ MediaForbiddenPublic,
1645
1796
  MediaUnsupportedPublic,
1646
1797
  MediaStorageFailedPublic,
1647
1798
  MediaEnrichmentFailedPublic,
1799
+ MediaAnalyticsWindowPublic,
1800
+ MediaAnalyticsUnavailablePublic,
1648
1801
  LeaderboardBoardNotFoundPublic,
1649
1802
  LeaderboardEntryNotFoundPublic,
1650
1803
  LeaderboardScoreRejectedPublic,
@@ -1707,6 +1860,8 @@ export const KitPublicErrorPayload = z
1707
1860
  PaymentsEntitlementNotInCatalogPublic,
1708
1861
  PaymentsSubjectUnresolvedPublic,
1709
1862
  PaymentsSubscriptionChangeRefusedPublic,
1863
+ PaymentsOverrideStalePublic,
1864
+ PaymentsQuotaExceededPublic,
1710
1865
  ControlPlaneNotConnectedPublic,
1711
1866
  ControlPlaneInvalidCredentialPublic,
1712
1867
  ControlPlaneInsufficientScopePublic,
@@ -1719,6 +1874,11 @@ export const KitPublicErrorPayload = z
1719
1874
  OrganizationLastAdministratorPublic,
1720
1875
  OrganizationSlugTakenPublic,
1721
1876
  OrganizationNominationInvalidPublic,
1877
+ ArtifactsNotFoundPublic,
1878
+ ArtifactsForbiddenPublic,
1879
+ ArtifactsInvalidNamePublic,
1880
+ ArtifactsNameTakenPublic,
1881
+ ArtifactsInvalidConfigPublic,
1722
1882
  SupportNotFoundPublic,
1723
1883
  SupportInvalidCategoryPublic,
1724
1884
  SupportUnparseableMessagePublic,
@@ -1815,6 +1975,12 @@ const TurnstileMissingToken = TurnstileMissingTokenPublic.extend(internalFields)
1815
1975
  );
1816
1976
  const TurnstileFailed = TurnstileFailedPublic.extend(internalFields).describe(TurnstileFailedPublic.description ?? "");
1817
1977
  const TurnstileConfig = TurnstileConfigPublic.extend(internalFields).describe(TurnstileConfigPublic.description ?? "");
1978
+ const TurnstileAnalyticsWindow = TurnstileAnalyticsWindowPublic.extend(internalFields).describe(
1979
+ TurnstileAnalyticsWindowPublic.description ?? "",
1980
+ );
1981
+ const TurnstileAnalyticsUnavailable = TurnstileAnalyticsUnavailablePublic.extend(internalFields).describe(
1982
+ TurnstileAnalyticsUnavailablePublic.description ?? "",
1983
+ );
1818
1984
  const AuditInvalidEvent = AuditInvalidEventPublic.extend(internalFields).describe(
1819
1985
  AuditInvalidEventPublic.description ?? "",
1820
1986
  );
@@ -1822,6 +1988,7 @@ const AuditWriteFailed = AuditWriteFailedPublic.extend(internalFields).describe(
1822
1988
  AuditWriteFailedPublic.description ?? "",
1823
1989
  );
1824
1990
  const MediaNotFound = MediaNotFoundPublic.extend(internalFields).describe(MediaNotFoundPublic.description ?? "");
1991
+ const MediaForbidden = MediaForbiddenPublic.extend(internalFields).describe(MediaForbiddenPublic.description ?? "");
1825
1992
  const MediaUnsupported = MediaUnsupportedPublic.extend(internalFields).describe(
1826
1993
  MediaUnsupportedPublic.description ?? "",
1827
1994
  );
@@ -1831,6 +1998,12 @@ const MediaStorageFailed = MediaStorageFailedPublic.extend(internalFields).descr
1831
1998
  const MediaEnrichmentFailed = MediaEnrichmentFailedPublic.extend(internalFields).describe(
1832
1999
  MediaEnrichmentFailedPublic.description ?? "",
1833
2000
  );
2001
+ const MediaAnalyticsWindow = MediaAnalyticsWindowPublic.extend(internalFields).describe(
2002
+ MediaAnalyticsWindowPublic.description ?? "",
2003
+ );
2004
+ const MediaAnalyticsUnavailable = MediaAnalyticsUnavailablePublic.extend(internalFields).describe(
2005
+ MediaAnalyticsUnavailablePublic.description ?? "",
2006
+ );
1834
2007
  const LeaderboardBoardNotFound = LeaderboardBoardNotFoundPublic.extend(internalFields).describe(
1835
2008
  LeaderboardBoardNotFoundPublic.description ?? "",
1836
2009
  );
@@ -2016,6 +2189,12 @@ const PaymentsSubjectUnresolved = PaymentsSubjectUnresolvedPublic.extend(interna
2016
2189
  const PaymentsSubscriptionChangeRefused = PaymentsSubscriptionChangeRefusedPublic.extend(internalFields).describe(
2017
2190
  PaymentsSubscriptionChangeRefusedPublic.description ?? "",
2018
2191
  );
2192
+ const PaymentsOverrideStale = PaymentsOverrideStalePublic.extend(internalFields).describe(
2193
+ PaymentsOverrideStalePublic.description ?? "",
2194
+ );
2195
+ const PaymentsQuotaExceeded = PaymentsQuotaExceededPublic.extend(internalFields).describe(
2196
+ PaymentsQuotaExceededPublic.description ?? "",
2197
+ );
2019
2198
  const ControlPlaneNotConnected = ControlPlaneNotConnectedPublic.extend(internalFields).describe(
2020
2199
  ControlPlaneNotConnectedPublic.description ?? "",
2021
2200
  );
@@ -2086,6 +2265,22 @@ const OrganizationNominationInvalid = OrganizationNominationInvalidPublic.extend
2086
2265
  OrganizationNominationInvalidPublic.description ?? "",
2087
2266
  );
2088
2267
 
2268
+ const ArtifactsNotFound = ArtifactsNotFoundPublic.extend(internalFields).describe(
2269
+ ArtifactsNotFoundPublic.description ?? "",
2270
+ );
2271
+ const ArtifactsForbidden = ArtifactsForbiddenPublic.extend(internalFields).describe(
2272
+ ArtifactsForbiddenPublic.description ?? "",
2273
+ );
2274
+ const ArtifactsInvalidName = ArtifactsInvalidNamePublic.extend(internalFields).describe(
2275
+ ArtifactsInvalidNamePublic.description ?? "",
2276
+ );
2277
+ const ArtifactsNameTaken = ArtifactsNameTakenPublic.extend(internalFields).describe(
2278
+ ArtifactsNameTakenPublic.description ?? "",
2279
+ );
2280
+ const ArtifactsInvalidConfig = ArtifactsInvalidConfigPublic.extend(internalFields).describe(
2281
+ ArtifactsInvalidConfigPublic.description ?? "",
2282
+ );
2283
+
2089
2284
  const SupportNotFound = SupportNotFoundPublic.extend(internalFields).describe(SupportNotFoundPublic.description ?? "");
2090
2285
  const SupportInvalidCategory = SupportInvalidCategoryPublic.extend(internalFields).describe(
2091
2286
  SupportInvalidCategoryPublic.description ?? "",
@@ -2145,12 +2340,17 @@ export const KitErrorPayload = z
2145
2340
  TurnstileMissingToken,
2146
2341
  TurnstileFailed,
2147
2342
  TurnstileConfig,
2343
+ TurnstileAnalyticsWindow,
2344
+ TurnstileAnalyticsUnavailable,
2148
2345
  AuditInvalidEvent,
2149
2346
  AuditWriteFailed,
2150
2347
  MediaNotFound,
2348
+ MediaForbidden,
2151
2349
  MediaUnsupported,
2152
2350
  MediaStorageFailed,
2153
2351
  MediaEnrichmentFailed,
2352
+ MediaAnalyticsWindow,
2353
+ MediaAnalyticsUnavailable,
2154
2354
  LeaderboardBoardNotFound,
2155
2355
  LeaderboardEntryNotFound,
2156
2356
  LeaderboardScoreRejected,
@@ -2213,6 +2413,8 @@ export const KitErrorPayload = z
2213
2413
  PaymentsEntitlementNotInCatalog,
2214
2414
  PaymentsSubjectUnresolved,
2215
2415
  PaymentsSubscriptionChangeRefused,
2416
+ PaymentsOverrideStale,
2417
+ PaymentsQuotaExceeded,
2216
2418
  ControlPlaneNotConnected,
2217
2419
  ControlPlaneInvalidCredential,
2218
2420
  ControlPlaneInsufficientScope,
@@ -2225,6 +2427,11 @@ export const KitErrorPayload = z
2225
2427
  OrganizationLastAdministrator,
2226
2428
  OrganizationSlugTaken,
2227
2429
  OrganizationNominationInvalid,
2430
+ ArtifactsNotFound,
2431
+ ArtifactsForbidden,
2432
+ ArtifactsInvalidName,
2433
+ ArtifactsNameTaken,
2434
+ ArtifactsInvalidConfig,
2228
2435
  SupportNotFound,
2229
2436
  SupportInvalidCategory,
2230
2437
  SupportUnparseableMessage,
@@ -15,12 +15,24 @@ import { Locale } from "../i18n/locale";
15
15
  * stored choice outrank their device's `Accept-Language`. That link read `c.var.locale` at first,
16
16
  * which nothing ever wrote, so it silently contributed nothing and the chain degraded to
17
17
  * `param → cookie → header → default` while the docs said otherwise.
18
+ *
19
+ * **`scopes` answers one question: did this request come from the adopter's own trusted backend, or from a
20
+ * device?** The name reads like a grant to the user, and it is not one. Tenancy roles went to `c.var.acting`
21
+ * with `@pithy-sh/organization`; management access is the control plane's, with its own credential and its
22
+ * own scope type. What is left is an attestation about the caller, which `@pithy-sh/auth`'s `resolveScopes`
23
+ * makes on every request from something the request proves — a service binding, a verified client
24
+ * certificate, a short-lived token bound to this origin — and never from anything it merely says (#560).
18
25
  */
19
26
  export const AuthContext = z
20
27
  .object({
21
28
  userId: z.string().describe("ID of the authenticated user (populated by @pithy-sh/auth)."),
22
29
  sessionId: z.string().describe("ID of the active session this request belongs to."),
23
- scopes: z.array(z.string()).default([]).describe("Permission scopes granted to this session."),
30
+ scopes: z
31
+ .array(z.string())
32
+ .default([])
33
+ .describe(
34
+ "Attestations about the caller, not permissions granted to the user: what this request's caller was established to be — the adopter's own trusted backend, say — by the `resolveScopes` seam on `@pithy-sh/auth`, resolved per request and never cached. Empty when no resolver is configured. Tenancy roles are not here; they are `c.var.acting`.",
35
+ ),
24
36
  locale: Locale.nullish().describe(
25
37
  "The reader's own stored language, from `pithy_auth_users.locale`; null or absent when they have never chosen.",
26
38
  ),
@@ -0,0 +1,34 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { Hono } from "hono";
5
+ import type { PithyHonoEnv, ScopeGate } from "../capability/capability";
6
+ import { routeSpecOf } from "./routeSpec";
7
+
8
+ /**
9
+ * **A capability's scope gates, read off its own routes rather than listed beside them (#560).**
10
+ *
11
+ * `Capability.scopeGates` is what `pithy doctor` names when nothing composed fills `AuthContext.scopes`.
12
+ * A list written out by hand next to the routes is a second copy of something the routes already say: every
13
+ * gated route carries `describeRoute({ scopes })`, because that is how the OpenAPI contract learns it. So the
14
+ * registrar is run against a throwaway app and the specs are read back — the same walk `openApiDocument`
15
+ * makes over a composed app, and a route whose scope changes changes what this returns with it.
16
+ *
17
+ * **Only `bearer` and `session` routes.** Those are the strategies `requireAuth()` takes, so a scope on one
18
+ * is an `AuthContext` scope. A `control-plane` route also names scopes, and they are a different credential
19
+ * entirely — a signed management token with its own type, never `c.var.auth` — so naming one here would tell
20
+ * an adopter to configure a resolver that could never open it.
21
+ */
22
+ export function scopeGatesOf(register: (app: Hono<PithyHonoEnv>) => void): ScopeGate[] {
23
+ const app = new Hono<PithyHonoEnv>();
24
+ register(app);
25
+ const gates: ScopeGate[] = [];
26
+ for (const route of app.routes) {
27
+ const spec = routeSpecOf(route.handler);
28
+ if (!spec?.scopes || spec.scopes.length === 0) continue;
29
+ const verification = [spec.verification].flat();
30
+ if (!verification.some((strategy) => strategy === "bearer" || strategy === "session")) continue;
31
+ for (const scope of spec.scopes) gates.push({ scope, route: `${route.method} ${route.path}` });
32
+ }
33
+ return gates;
34
+ }
package/src/index.ts CHANGED
@@ -101,6 +101,7 @@ export {
101
101
  ControlPlaneScope,
102
102
  KEYS_ROTATE_SCOPE,
103
103
  MANIFEST_READ_SCOPE,
104
+ MIGRATIONS_READ_SCOPE,
104
105
  SEAM_SCOPES,
105
106
  } from "./controlPlane/scope/scope";
106
107
  export { exportPublicJwk, type MintControlPlaneToken, mintControlPlaneToken } from "./controlPlane/token/mint";
@@ -0,0 +1,87 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import type { MigrationProvider } from "kysely/migration";
5
+ import type { Capability } from "../capability/capability";
6
+ import type { DatabaseSpec } from "../data/databases";
7
+ import { InternalError } from "../error/pithyError";
8
+ import { createMigrationRegistry, type NamespacedMigrations } from "./registry";
9
+
10
+ /**
11
+ * **Where a capability's migrations become a registry's sets — one function, composed twice (#729).**
12
+ *
13
+ * `pithy migrate` and `pithy doctor` compose these sets from the project on disk; the Worker composes them
14
+ * from the capabilities it was built with, to answer `GET {basePath}/migrations`. It lived in the CLI until
15
+ * the Worker needed it too. It reads nothing but `Capability.databases`, so it belongs in core — and if
16
+ * the two sides composed with two functions, `pithy doctor` and the route could disagree about one
17
+ * environment with nothing to say which was wrong.
18
+ */
19
+
20
+ /**
21
+ * A retained declaration must name a table this slice declares, beside the migrations that create it.
22
+ *
23
+ * A misspelled key would count a table that does not exist, find no rows, and let the `down` through —
24
+ * the refusal quietly off for the one table it was written for. And retention on a slice with no
25
+ * migrations has no `down` to be recorded on, so nothing would ever enforce it. Both are an author's mistake,
26
+ * caught where the declaration is read rather than discovered when a vault is empty (#588).
27
+ */
28
+ function assertRetainedDeclared(capability: string, database: string, spec: DatabaseSpec): void {
29
+ const retained = spec.retained ?? [];
30
+ if (retained.length === 0) return;
31
+ const unknown = retained.filter((table) => !(table in spec.tables));
32
+ if (unknown.length > 0) {
33
+ throw new InternalError({
34
+ message: `Capability "${capability}" retains ${unknown.map((table) => `"${table}"`).join(", ")} in database "${database}", which its tables do not declare.`,
35
+ action: "Name tables from that database spec's tables in retained.",
36
+ });
37
+ }
38
+ if (!spec.migrations || Object.keys(spec.migrations).length === 0) {
39
+ throw new InternalError({
40
+ message: `Capability "${capability}" retains tables in database "${database}" but ships no migrations for it.`,
41
+ action: "Declare retained beside the migrations that create those tables.",
42
+ });
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Every capability's per-database migration set (`DatabaseSpec.migrations`, namespaced by capability
48
+ * name), in composition order — the raw material {@link buildRegistryFromCapabilities}, the CLI's
49
+ * fan-out runner and the control-plane seam all build on. The runner needs the sets rather than a
50
+ * finished provider because two Workers can be bound to one physical D1: their sets merge into a single
51
+ * ordered provider so that database migrates once, with every Worker's capabilities in it. A Worker
52
+ * composes only its own, which is why the seam's `undeclared` is relative to that Worker. Migrations
53
+ * without a `migrationOrder` fail here, attributed to their capability.
54
+ */
55
+ export function collectMigrationSets(capabilities: Capability[]): NamespacedMigrations[] {
56
+ const sets: NamespacedMigrations[] = [];
57
+ for (const capability of capabilities) {
58
+ for (const [database, spec] of Object.entries(capability.databases ?? {})) {
59
+ assertRetainedDeclared(capability.name, database, spec);
60
+ if (!spec.migrations) continue;
61
+ if (spec.migrationOrder === undefined) {
62
+ throw new InternalError({
63
+ message: `Capability "${capability.name}" has migrations for database "${database}" but no migrationOrder.`,
64
+ action: "Set migrationOrder on that database spec (core low, app high).",
65
+ });
66
+ }
67
+ sets.push({
68
+ database,
69
+ namespace: capability.name,
70
+ order: spec.migrationOrder,
71
+ migrations: spec.migrations,
72
+ ...(spec.retained && spec.retained.length > 0 ? { retained: spec.retained } : {}),
73
+ });
74
+ }
75
+ }
76
+ return sets;
77
+ }
78
+
79
+ /**
80
+ * Compose every capability's per-database migration sets (`DatabaseSpec.migrations`,
81
+ * namespaced by capability name) into one ordered provider per database. A database
82
+ * nobody migrates gets no provider; migrations without a `migrationOrder` fail here,
83
+ * attributed to their capability.
84
+ */
85
+ export function buildRegistryFromCapabilities(capabilities: Capability[]): Record<string, MigrationProvider> {
86
+ return createMigrationRegistry(collectMigrationSets(capabilities));
87
+ }
@@ -62,6 +62,51 @@ function assertValidLocalKey(localKey: string, namespace: string): void {
62
62
  }
63
63
  }
64
64
 
65
+ /**
66
+ * The composed key for one of a set's migrations: `NNNN_<namespace>_<localKey>`. The one place the shape is
67
+ * written, so the registry and anything naming a declared migration's namespace build the same string.
68
+ */
69
+ export function composedMigrationKey(set: Pick<NamespacedMigrations, "order" | "namespace">, localKey: string): string {
70
+ return `${String(set.order).padStart(ORDER_DIGITS, "0")}_${set.namespace}_${localKey}`;
71
+ }
72
+
73
+ /** A composed key taken back apart: the three parts {@link createMigrationRegistry} joined. */
74
+ export interface ComposedMigrationKey {
75
+ /** The capability's `migrationOrder` — the zero-padded prefix, as a number. */
76
+ order: number;
77
+ /** The capability namespace — its `name`, which the manifest reports beside every capability. */
78
+ namespace: string;
79
+ /** The capability's own stable key, e.g. `0001_init`. */
80
+ localKey: string;
81
+ }
82
+
83
+ /**
84
+ * The composed key shape, anchored at both ends: the order prefix, a namespace (which admits no separator,
85
+ * so the first underscore after it is the boundary), then a local key, which may hold any number.
86
+ */
87
+ const COMPOSED_KEY_PATTERN = new RegExp(
88
+ `^(\\d{${ORDER_DIGITS}})_(${NAMESPACE_PATTERN.source.slice(1, -1)})_(${LOCAL_KEY_PATTERN.source.slice(1, -1)})$`,
89
+ );
90
+
91
+ /**
92
+ * Take a composed key back apart — the inverse of the composition below.
93
+ *
94
+ * A declared migration's namespace is known from the set it came from. An **undeclared** one — applied in
95
+ * the database, carried by no provider this build composes — has nothing but its key, so this is the only
96
+ * way to say whose it was. **Null for a key no registry could have composed**, a migration written by hand
97
+ * or by another tool: guessing a namespace for it would be inventing one.
98
+ *
99
+ * The patterns are the same constants the composition enforces, so the two cannot disagree about what a
100
+ * key looks like.
101
+ */
102
+ export function decomposeMigrationKey(key: string): ComposedMigrationKey | null {
103
+ const match = COMPOSED_KEY_PATTERN.exec(key);
104
+ if (!match) return null;
105
+ const [, order, namespace, localKey] = match;
106
+ if (order === undefined || namespace === undefined || localKey === undefined) return null;
107
+ return { order: Number(order), namespace, localKey };
108
+ }
109
+
65
110
  /** Merge one database's sets into a provider, enforcing the per-database invariants. */
66
111
  function composeDatabase(database: string, sets: NamespacedMigrations[]): MigrationProvider {
67
112
  const seenOrders = new Set<number>();
@@ -85,10 +130,9 @@ function composeDatabase(database: string, sets: NamespacedMigrations[]): Migrat
85
130
 
86
131
  const composed: Record<string, Migration> = {};
87
132
  for (const set of [...sets].sort((a, b) => a.order - b.order)) {
88
- const prefix = String(set.order).padStart(ORDER_DIGITS, "0");
89
133
  for (const localKey of Object.keys(set.migrations).sort()) {
90
134
  const migration = set.migrations[localKey];
91
- if (migration) composed[`${prefix}_${set.namespace}_${localKey}`] = migration;
135
+ if (migration) composed[composedMigrationKey(set, localKey)] = migration;
92
136
  }
93
137
  }
94
138
 
@@ -0,0 +1,94 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import type { D1Database } from "@cloudflare/workers-types";
5
+ import type { MigrationProvider } from "kysely/migration";
6
+ import { appliedMigrationChain, migrationKysely } from "./bookkeeping";
7
+ import { readGroupPosition, readMigrationGroups } from "./groups";
8
+ import { readMigrationLedger } from "./runner";
9
+
10
+ /**
11
+ * **Where one database stands, read in one place (#729).**
12
+ *
13
+ * The control-plane seam answers `GET {basePath}/migrations` from this: what is applied, what this build
14
+ * declares and the database lacks, what the database holds and this build no longer declares, and which
15
+ * run put it there. Each half already had a reader — {@link readMigrationLedger} for both directions,
16
+ * {@link readMigrationGroups} and {@link readGroupPosition} for the group (#694) — and this composes them
17
+ * rather than reading either table a second way.
18
+ *
19
+ * Read-only. It applies nothing, creates nothing, and reverses nothing: a screen somebody opened to *ask*
20
+ * must not be the thing that changes the schema.
21
+ */
22
+
23
+ /** Whether the last group can still be taken back by name — {@link readGroupPosition}, minus `absent`. */
24
+ export type MigrationGroupStanding =
25
+ | {
26
+ /** The group's migrations are the contiguous tip of this ledger. The CLI's rollback has refusals of its own. */
27
+ state: "top";
28
+ }
29
+ | {
30
+ /** Something sits over the group or inside it, so reversing it by name is refused. */
31
+ state: "buried";
32
+ /** The groups in the way, newest first. */
33
+ above: string[];
34
+ /** The migrations in the way that no group claims, newest first. */
35
+ ungrouped: string[];
36
+ };
37
+
38
+ /** The newest group with anything still applied here, and whether it is still reversible. */
39
+ export interface MigrationLastGroup {
40
+ /** The group's value — what `--rollback --group` takes. */
41
+ group: string;
42
+ /** When it last applied something here. */
43
+ appliedAt: Date;
44
+ /** What it applied here and is still applied, in application order. */
45
+ migrations: string[];
46
+ /** Whether it is the tip of the chain, or what is in its way. */
47
+ position: MigrationGroupStanding;
48
+ }
49
+
50
+ /** One database's migration state. */
51
+ export interface MigrationState {
52
+ /** Every applied migration, in the order the ledger applied them. */
53
+ applied: string[];
54
+ /** Declared and not yet applied, in the order they would run. */
55
+ pending: string[];
56
+ /** Applied and not declared by this build, sorted. Kysely refuses the whole chain on any one of these. */
57
+ undeclared: string[];
58
+ /** The newest group with anything applied here, or null when nothing applied here was ever grouped. */
59
+ lastGroup: MigrationLastGroup | null;
60
+ }
61
+
62
+ /**
63
+ * Read one database's migration state against the provider this build composes for it.
64
+ *
65
+ * **The last group is the newest one with something still applied.** Group rows are forgotten as their
66
+ * migrations come down, so this is ordinarily the newest group recorded. A group whose rows outlived their
67
+ * ledger rows reads as `absent` and is passed over: naming it as where the environment stands would name a
68
+ * release that is no longer in the database.
69
+ */
70
+ export async function readMigrationState(database: D1Database, provider: MigrationProvider): Promise<MigrationState> {
71
+ const { pending, undeclared } = await readMigrationLedger(database, provider);
72
+ const applied = await appliedMigrationChain(migrationKysely(database));
73
+ return { applied, pending, undeclared, lastGroup: await lastGroupOf(database, applied) };
74
+ }
75
+
76
+ /** Walk the groups newest first and stop at the first one this database still holds any of. */
77
+ async function lastGroupOf(database: D1Database, applied: readonly string[]): Promise<MigrationLastGroup | null> {
78
+ const held = new Set(applied);
79
+ const groups = await readMigrationGroups(database);
80
+ for (const group of [...groups].reverse()) {
81
+ const position = await readGroupPosition(database, group.group);
82
+ if (position.state === "absent") continue;
83
+ return {
84
+ group: group.group,
85
+ appliedAt: group.appliedAt,
86
+ migrations: group.migrations.filter((name) => held.has(name)),
87
+ position:
88
+ position.state === "top"
89
+ ? { state: "top" }
90
+ : { state: "buried", above: position.above, ungrouped: position.ungrouped },
91
+ };
92
+ }
93
+ return null;
94
+ }