@pithy-sh/payments 0.1.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 (167) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +157 -0
  3. package/dist/paddle-prices.iife.js +1 -0
  4. package/dist/pithy.manifest.json +68 -0
  5. package/docs/lemon-squeezy.md +167 -0
  6. package/docs/paddle.md +486 -0
  7. package/package.json +73 -0
  8. package/pithy.manifest.json +68 -0
  9. package/src/admin/coverage.ts +107 -0
  10. package/src/admin/read.ts +360 -0
  11. package/src/audit/actions.ts +214 -0
  12. package/src/capability.ts +377 -0
  13. package/src/client/api.ts +734 -0
  14. package/src/client/checkout.ts +252 -0
  15. package/src/client/fixtures/price-preview-de.json +54 -0
  16. package/src/client/fixtures/price-preview-gb.json +54 -0
  17. package/src/client/fixtures/price-preview-jp-yen.json +54 -0
  18. package/src/client/fixtures/price-preview-us-country-only.json +54 -0
  19. package/src/client/fixtures/price-preview-us-new-york.json +54 -0
  20. package/src/client/fixtures/pricePreview.ts +67 -0
  21. package/src/client/hooks.ts +590 -0
  22. package/src/client/paddle.ts +733 -0
  23. package/src/client/paddleCache.ts +265 -0
  24. package/src/client/paddlePrices.iife.ts +43 -0
  25. package/src/client/paddlePrices.ts +143 -0
  26. package/src/client/paddlePricesTag.ts +332 -0
  27. package/src/client/projection.ts +133 -0
  28. package/src/client/test-utils/cacheStore.ts +41 -0
  29. package/src/client/wholeUnits.ts +147 -0
  30. package/src/cloudflare-test.d.ts +16 -0
  31. package/src/config/config.ts +563 -0
  32. package/src/data/discount.ts +238 -0
  33. package/src/data/entitlement.ts +67 -0
  34. package/src/data/money.ts +75 -0
  35. package/src/data/pause.ts +148 -0
  36. package/src/data/providerAccount.ts +47 -0
  37. package/src/data/purchase.ts +131 -0
  38. package/src/data/rail.ts +74 -0
  39. package/src/data/reconcileRun.ts +208 -0
  40. package/src/data/renderMoney.ts +180 -0
  41. package/src/data/status.ts +67 -0
  42. package/src/data/subject.ts +148 -0
  43. package/src/data/subscription.ts +675 -0
  44. package/src/data/syncCursor.ts +44 -0
  45. package/src/data/tables.ts +61 -0
  46. package/src/data/webhookEvent.ts +196 -0
  47. package/src/entitlement/manual.ts +209 -0
  48. package/src/entitlement/resolver.ts +125 -0
  49. package/src/entitlement/subjectSeam.ts +171 -0
  50. package/src/error/errors.ts +421 -0
  51. package/src/grants/apply.ts +207 -0
  52. package/src/grants/clawback.ts +128 -0
  53. package/src/grants/ledgerSeam.ts +141 -0
  54. package/src/http/dispatch.ts +48 -0
  55. package/src/http/guards.ts +58 -0
  56. package/src/http/manifestConfig.ts +56 -0
  57. package/src/http/responses.ts +1147 -0
  58. package/src/http/routes.ts +2911 -0
  59. package/src/http/schemas.ts +577 -0
  60. package/src/http/scopes.ts +260 -0
  61. package/src/http/view.ts +188 -0
  62. package/src/http/webhookGuard.ts +341 -0
  63. package/src/index.ts +231 -0
  64. package/src/migrations/0001_purchases.ts +312 -0
  65. package/src/pricing/location.ts +175 -0
  66. package/src/pricing/visitor.ts +82 -0
  67. package/src/projection/event.ts +101 -0
  68. package/src/projection/orphans.ts +226 -0
  69. package/src/projection/owner.ts +247 -0
  70. package/src/projection/resolve.ts +57 -0
  71. package/src/projection/writer.ts +554 -0
  72. package/src/provision/resolvePaymentsConfig.ts +91 -0
  73. package/src/rails/apple/certs.ts +66 -0
  74. package/src/rails/apple/der.ts +249 -0
  75. package/src/rails/apple/fixtures/chain.ts +412 -0
  76. package/src/rails/apple/fixtures/did-fail-to-renew-grace.json +43 -0
  77. package/src/rails/apple/fixtures/did-renew.json +48 -0
  78. package/src/rails/apple/fixtures/one-time-charge.json +32 -0
  79. package/src/rails/apple/fixtures/refund.json +33 -0
  80. package/src/rails/apple/fixtures/subscribed-initial-buy-sandbox.json +38 -0
  81. package/src/rails/apple/fixtures/test.json +14 -0
  82. package/src/rails/apple/http.ts +128 -0
  83. package/src/rails/apple/jws.ts +123 -0
  84. package/src/rails/apple/notification.ts +357 -0
  85. package/src/rails/apple/rail.ts +114 -0
  86. package/src/rails/apple/serverApi.ts +412 -0
  87. package/src/rails/apple/verify.ts +93 -0
  88. package/src/rails/apple/x509.ts +424 -0
  89. package/src/rails/contract.ts +939 -0
  90. package/src/rails/google/fixtures/play-product-purchased.json +14 -0
  91. package/src/rails/google/fixtures/play-subscription-active.json +23 -0
  92. package/src/rails/google/fixtures/push.ts +152 -0
  93. package/src/rails/google/fixtures/rtdn-one-time-purchased.json +11 -0
  94. package/src/rails/google/fixtures/rtdn-subscription-renewed.json +11 -0
  95. package/src/rails/google/fixtures/rtdn-subscription-revoked.json +11 -0
  96. package/src/rails/google/fixtures/rtdn-test.json +8 -0
  97. package/src/rails/google/fixtures/rtdn-voided-purchase.json +11 -0
  98. package/src/rails/google/http.ts +124 -0
  99. package/src/rails/google/jwt.ts +127 -0
  100. package/src/rails/google/oidc.ts +308 -0
  101. package/src/rails/google/playApi.ts +598 -0
  102. package/src/rails/google/rail.ts +174 -0
  103. package/src/rails/google/rtdn.ts +331 -0
  104. package/src/rails/google/verify.ts +127 -0
  105. package/src/rails/lemonSqueezy/api.ts +192 -0
  106. package/src/rails/lemonSqueezy/checkout.ts +186 -0
  107. package/src/rails/lemonSqueezy/discounts.ts +166 -0
  108. package/src/rails/lemonSqueezy/fixtures/events.ts +180 -0
  109. package/src/rails/lemonSqueezy/objects.ts +511 -0
  110. package/src/rails/lemonSqueezy/portal.ts +82 -0
  111. package/src/rails/lemonSqueezy/pricing.ts +124 -0
  112. package/src/rails/lemonSqueezy/rail.ts +137 -0
  113. package/src/rails/lemonSqueezy/read.ts +72 -0
  114. package/src/rails/lemonSqueezy/refresh.ts +86 -0
  115. package/src/rails/lemonSqueezy/signature.ts +119 -0
  116. package/src/rails/lemonSqueezy/verify.ts +45 -0
  117. package/src/rails/lemonSqueezy/webhook.ts +326 -0
  118. package/src/rails/paddle/adjustments.ts +94 -0
  119. package/src/rails/paddle/api.ts +266 -0
  120. package/src/rails/paddle/checkout.ts +240 -0
  121. package/src/rails/paddle/discounts.ts +176 -0
  122. package/src/rails/paddle/events.ts +285 -0
  123. package/src/rails/paddle/fixtures/browserForged.ts +54 -0
  124. package/src/rails/paddle/fixtures/pausedSubscription.ts +62 -0
  125. package/src/rails/paddle/objects.ts +883 -0
  126. package/src/rails/paddle/portal.ts +130 -0
  127. package/src/rails/paddle/rail.ts +219 -0
  128. package/src/rails/paddle/read.ts +108 -0
  129. package/src/rails/paddle/recorded.ts +83 -0
  130. package/src/rails/paddle/refresh.ts +153 -0
  131. package/src/rails/paddle/refund.ts +297 -0
  132. package/src/rails/paddle/signature.ts +225 -0
  133. package/src/rails/paddle/subscription.ts +793 -0
  134. package/src/rails/paddle/verify.ts +120 -0
  135. package/src/rails/paddle/webhook.ts +405 -0
  136. package/src/rails/providers.ts +168 -0
  137. package/src/rails/stripe/api.ts +253 -0
  138. package/src/rails/stripe/checkout.ts +175 -0
  139. package/src/rails/stripe/discounts.ts +169 -0
  140. package/src/rails/stripe/fixtures/event-charge-refunded.json +23 -0
  141. package/src/rails/stripe/fixtures/event-invoice-paid.json +19 -0
  142. package/src/rails/stripe/fixtures/event-session-completed-payment.json +27 -0
  143. package/src/rails/stripe/fixtures/event-session-completed-subscription.json +27 -0
  144. package/src/rails/stripe/fixtures/event-subscription-canceled.json +43 -0
  145. package/src/rails/stripe/fixtures/event-subscription-created.json +42 -0
  146. package/src/rails/stripe/fixtures/event-subscription-deleted.json +44 -0
  147. package/src/rails/stripe/fixtures/events.ts +111 -0
  148. package/src/rails/stripe/objects.ts +642 -0
  149. package/src/rails/stripe/portal.ts +45 -0
  150. package/src/rails/stripe/pricing.ts +123 -0
  151. package/src/rails/stripe/rail.ts +139 -0
  152. package/src/rails/stripe/refresh.ts +79 -0
  153. package/src/rails/stripe/signature.ts +127 -0
  154. package/src/rails/stripe/verify.ts +105 -0
  155. package/src/rails/stripe/webhook.ts +79 -0
  156. package/src/secret/registry.ts +275 -0
  157. package/src/seeds/example.ts +181 -0
  158. package/src/version.generated.ts +16 -0
  159. package/src/workflows/paddleSweep.ts +717 -0
  160. package/src/workflows/railAccess.ts +87 -0
  161. package/src/workflows/reconcile.ts +648 -0
  162. package/src/workflows/report.ts +64 -0
  163. package/src/workflows/retryPolicy.ts +40 -0
  164. package/src/workflows/specs.ts +143 -0
  165. package/src/workflows/worker.ts +166 -0
  166. package/src/workflows/workerConfig.ts +73 -0
  167. package/src/workflows/wrangler.jsonc +55 -0
@@ -0,0 +1,66 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ /**
5
+ * Apple's root certificates, pinned as shipped assets.
6
+ *
7
+ * **These are not secrets.** A root certificate is a public key with a name on it; Apple publishes these
8
+ * for exactly this purpose, and every device that has ever talked to the App Store carries them. They are
9
+ * pinned rather than read from a trust store because the Workers runtime has no trust store to read, and
10
+ * because pinning is what makes the check mean something: verifying a chain against "whatever roots are
11
+ * around" would accept a notification signed by any CA, and a webhook endpoint that accepts any CA's
12
+ * signature accepts anyone's notification.
13
+ *
14
+ * ## Where these came from
15
+ *
16
+ * `APPLE_ROOT_CA_G3` is `AppleRootCA-G3.cer`, downloaded from Apple's published certificate authority page
17
+ * and base64-encoded from its DER form:
18
+ *
19
+ * ```sh
20
+ * curl -O https://www.apple.com/certificateauthority/AppleRootCA-G3.cer
21
+ * openssl x509 -inform der -in AppleRootCA-G3.cer -outform pem | sed -e '1d' -e '$d' | tr -d '\n'
22
+ * ```
23
+ *
24
+ * Verify a copy before trusting it. The SHA-256 fingerprint is
25
+ * `63:34:3A:BF:B8:9A:6A:03:EB:B5:7E:9B:3F:5F:A7:BE:7C:4F:5C:75:6F:30:17:B3:A8:C4:88:C3:65:3E:91:79`,
26
+ * the serial is `2DC5FC88D2C54B95`, and it is valid from 2014-04-30 to 2039-04-30:
27
+ *
28
+ * ```sh
29
+ * openssl x509 -inform der -in AppleRootCA-G3.cer -noout -fingerprint -sha256 -serial -dates
30
+ * ```
31
+ *
32
+ * ## How to refresh them
33
+ *
34
+ * This is a `readonly string[]` and not a single value because a root rotation is additive, not a swap.
35
+ * Apple would publish a new root long before retiring G3, notifications would arrive signed under either,
36
+ * and both must verify during the overlap. So: add the new root's base64 to the array with its fingerprint
37
+ * recorded above, ship it, and remove the old one only once Apple has retired it. Removing first is what
38
+ * turns a routine rotation into an outage in which every renewal notification is refused.
39
+ *
40
+ * G3 expires in 2039. That is a long time, and it is exactly why the refresh procedure is written down
41
+ * here rather than remembered: nobody who ships this will be the one who has to do it.
42
+ *
43
+ * ## What the chain looks like
44
+ *
45
+ * An App Store Server Notification's JWS carries the whole chain in its `x5c` header, leaf first:
46
+ *
47
+ * leaf (ECDSA P-256, signed with SHA-256)
48
+ * ← Apple Worldwide Developer Relations Certification Authority, OU=G6 (ECDSA P-384, SHA-384)
49
+ * ← Apple Root CA - G3 (ECDSA P-384, SHA-384, self-signed)
50
+ *
51
+ * The chain is elliptic-curve end to end, which is why the verifier accepts ECDSA and nothing else. An
52
+ * RSA-signed chain would be refused, and that is deliberate: an algorithm the boundary never sees is
53
+ * surface with no user. If Apple ever publishes an RSA root, it arrives through the refresh procedure
54
+ * above alongside support for it.
55
+ */
56
+
57
+ /** Apple Root CA - G3, DER, base64. The root every App Store Server Notification chains to. */
58
+ const APPLE_ROOT_CA_G3 =
59
+ "MIICQzCCAcmgAwIBAgIILcX8iNLFS5UwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwSQXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwHhcNMTQwNDMwMTgxOTA2WhcNMzkwNDMwMTgxOTA2WjBnMRswGQYDVQQDDBJBcHBsZSBSb290IENBIC0gRzMxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzB2MBAGByqGSM49AgEGBSuBBAAiA2IABJjpLz1AcqTtkyJygRMc3RCV8cWjTnHcFBbZDuWmBSp3ZHtfTjjTuxxEtX/1H7YyYl3J6YRbTzBPEVoA/VhYDKX1DyxNB0cTddqXl5dvMVztK517IDvYuVTZXpmkOlEKMaNCMEAwHQYDVR0OBBYEFLuw3qFYM4iapIqZ3r6966/ayySrMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMQCD6cHEFl4aXTQY2e3v9GwOAEZLuN+yRhHFD/3meoyhpmvOwgPUnPWTxnS4at+qIxUCMG1mihDK1A3UT82NQz60imOlM27jbdoXt2QfyFMm+YhidDkLF1vLUagM6BgD56KyKA==";
60
+
61
+ /**
62
+ * Every Apple root a chain may terminate in, base64 DER. The last certificate in a JWS `x5c` must be
63
+ * byte-identical to one of these — a stronger check than re-verifying a self-signature, which only proves
64
+ * the root vouches for itself.
65
+ */
66
+ export const APPLE_ROOT_CERTIFICATES: readonly string[] = [APPLE_ROOT_CA_G3];
@@ -0,0 +1,249 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { PaymentsInvalidReceiptError } from "../../error/errors";
5
+
6
+ /**
7
+ * The byte encodings Apple's signed data is built from: base64 and base64url, and just enough ASN.1 DER
8
+ * to walk an X.509 certificate.
9
+ *
10
+ * **Why this exists at all.** Verifying an App Store Server Notification means verifying a JWS against a
11
+ * certificate chain, and there is no certificate parser in the Workers runtime — `crypto.subtle` imports a
12
+ * SubjectPublicKeyInfo but will not open a certificate to find one. Node's `X509Certificate` is not
13
+ * available here. So the parts of DER the chain needs are read directly, and nothing more: this is a
14
+ * reader, never a general ASN.1 library, and it has no encoder.
15
+ *
16
+ * **Every parse is hostile-input parsing.** These bytes arrive in an unauthenticated HTTP request, which
17
+ * is the whole point of verifying them, so every length is checked against the buffer it claims to live in
18
+ * before it is trusted. A length that overruns its buffer or its parent is refused rather than clamped —
19
+ * clamping would let a crafted certificate present bytes that were never inside it. Indefinite lengths are
20
+ * refused outright: DER forbids them, and honoring BER's end-of-contents marker is how a parser is made
21
+ * to disagree with the verifier about where a structure ends.
22
+ *
23
+ * Failures throw `payments/invalid_receipt` — malformed, not merely unverified. A caller on the webhook
24
+ * path maps that to `payments/webhook_unverified`, so one implementation serves both paths with the right
25
+ * code on each.
26
+ */
27
+
28
+ /** DER tag for a constructed SEQUENCE. */
29
+ export const ASN1_SEQUENCE = 0x30;
30
+ /** DER tag for an INTEGER. */
31
+ export const ASN1_INTEGER = 0x02;
32
+ /** DER tag for a BIT STRING. */
33
+ export const ASN1_BIT_STRING = 0x03;
34
+ /** DER tag for an OBJECT IDENTIFIER. */
35
+ export const ASN1_OID = 0x06;
36
+ /** DER tag for a UTCTime. */
37
+ export const ASN1_UTC_TIME = 0x17;
38
+ /** DER tag for a GeneralizedTime. */
39
+ export const ASN1_GENERALIZED_TIME = 0x18;
40
+ /** DER tag for a BOOLEAN — how an extension states `critical`, and how basicConstraints states `cA`. */
41
+ export const ASN1_BOOLEAN = 0x01;
42
+ /** DER tag for an OCTET STRING — an X.509 extension's value is one, wrapping the extension's own DER. */
43
+ export const ASN1_OCTET_STRING = 0x04;
44
+
45
+ /** The bit in a tag byte that marks a node constructed (its contents are further nodes). */
46
+ const ASN1_CONSTRUCTED = 0x20;
47
+
48
+ /** One ASN.1 node: its tag, the whole encoding, and the contents. */
49
+ export interface Asn1Node {
50
+ /** The tag byte, context-specific tags included (`0xa0` for `[0]`). */
51
+ tag: number;
52
+ /**
53
+ * The node's whole DER encoding, tag and length bytes included. This is the field a signature covers —
54
+ * a certificate's `tbsCertificate` is signed as encoded, not as contents — so it is kept rather than
55
+ * reconstructed.
56
+ */
57
+ der: Uint8Array;
58
+ /** The contents octets, without the tag or length. */
59
+ value: Uint8Array;
60
+ }
61
+
62
+ /** A structural refusal. The detail names the position, because a chain has several places to fail. */
63
+ function malformed(detail: string): PaymentsInvalidReceiptError {
64
+ return new PaymentsInvalidReceiptError({
65
+ message: "That purchase could not be read.",
66
+ action: "Submit the transaction exactly as the store SDK returned it.",
67
+ detail,
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Read one DER node at `offset`. Returns the node and the offset just past it, so a caller walks a
73
+ * sequence by feeding the returned `end` back in.
74
+ */
75
+ export function readAsn1(bytes: Uint8Array, offset = 0): { node: Asn1Node; end: number } {
76
+ if (offset + 2 > bytes.length) throw malformed(`DER: truncated header at offset ${offset}.`);
77
+ const tag = bytes[offset] as number;
78
+ const first = bytes[offset + 1] as number;
79
+ let length: number;
80
+ let contentsAt: number;
81
+
82
+ if (first < 0x80) {
83
+ length = first;
84
+ contentsAt = offset + 2;
85
+ } else {
86
+ const lengthBytes = first & 0x7f;
87
+ // 0x80 is BER's indefinite length. DER forbids it, and a reader that accepted it would have to trust
88
+ // an end-of-contents marker inside the very data it is checking.
89
+ if (lengthBytes === 0) throw malformed(`DER: indefinite length at offset ${offset}.`);
90
+ // Four bytes is 4 GiB; anything wider cannot be a real length and could overflow the accumulator.
91
+ if (lengthBytes > 4) throw malformed(`DER: length of ${lengthBytes} bytes at offset ${offset} is too wide.`);
92
+ if (offset + 2 + lengthBytes > bytes.length) throw malformed(`DER: truncated length at offset ${offset}.`);
93
+ length = 0;
94
+ for (let i = 0; i < lengthBytes; i += 1) length = length * 256 + (bytes[offset + 2 + i] as number);
95
+ contentsAt = offset + 2 + lengthBytes;
96
+ }
97
+
98
+ const end = contentsAt + length;
99
+ if (end > bytes.length) {
100
+ throw malformed(`DER: node at offset ${offset} claims ${length} bytes, ${bytes.length - contentsAt} remain.`);
101
+ }
102
+ return { node: { tag, der: bytes.subarray(offset, end), value: bytes.subarray(contentsAt, end) }, end };
103
+ }
104
+
105
+ /**
106
+ * The members of a constructed node, in order. Each member is read from the parent's contents, so a member
107
+ * that overruns the parent is refused — the check the parent's own length would otherwise not enforce.
108
+ */
109
+ export function asn1Children(node: Asn1Node): Asn1Node[] {
110
+ if ((node.tag & ASN1_CONSTRUCTED) === 0) throw malformed(`DER: tag 0x${node.tag.toString(16)} is not constructed.`);
111
+ const children: Asn1Node[] = [];
112
+ let offset = 0;
113
+ while (offset < node.value.length) {
114
+ const { node: child, end } = readAsn1(node.value, offset);
115
+ children.push(child);
116
+ offset = end;
117
+ }
118
+ return children;
119
+ }
120
+
121
+ /**
122
+ * An OBJECT IDENTIFIER as dotted decimal. Algorithm and curve identities are compared as strings rather
123
+ * than as byte patterns, so the comparison sites read as the names they mean.
124
+ */
125
+ export function readOid(node: Asn1Node): string {
126
+ if (node.tag !== ASN1_OID)
127
+ throw malformed(`DER: expected an OBJECT IDENTIFIER, found tag 0x${node.tag.toString(16)}.`);
128
+ if (node.value.length === 0) throw malformed("DER: empty OBJECT IDENTIFIER.");
129
+ const first = node.value[0] as number;
130
+ // The first two arcs share one byte: 40 * arc1 + arc2, with arc1 capped at 2.
131
+ const arcs: number[] = [Math.min(Math.floor(first / 40), 2)];
132
+ arcs.push(first - (arcs[0] as number) * 40);
133
+ let current = 0;
134
+ for (let i = 1; i < node.value.length; i += 1) {
135
+ const byte = node.value[i] as number;
136
+ current = current * 128 + (byte & 0x7f);
137
+ // The high bit means "this arc continues into the next byte".
138
+ if ((byte & 0x80) === 0) {
139
+ arcs.push(current);
140
+ current = 0;
141
+ }
142
+ }
143
+ return arcs.join(".");
144
+ }
145
+
146
+ /**
147
+ * An X.509 ECDSA signature (`SEQUENCE { INTEGER r, INTEGER s }`) as the fixed-width `r || s` WebCrypto
148
+ * verifies. `size` is the curve's coordinate size — 32 bytes for P-256, 48 for P-384.
149
+ *
150
+ * The two encodings differ in exactly the way that trips people up. DER writes each integer with the
151
+ * fewest bytes that keep it positive, so a coordinate whose top bit is set gains a leading zero and a
152
+ * small one loses leading bytes. WebCrypto wants both coordinates at full width. So the padding byte is
153
+ * stripped and the remainder right-aligned; a value still wider than the curve after stripping is refused
154
+ * rather than truncated, because truncating a signature makes a forgery verify.
155
+ */
156
+ export function derSignatureToRaw(der: Uint8Array, size: number): Uint8Array {
157
+ const { node } = readAsn1(der);
158
+ if (node.tag !== ASN1_SEQUENCE) throw malformed("DER: an ECDSA signature must be a SEQUENCE.");
159
+ const parts = asn1Children(node);
160
+ if (parts.length !== 2) throw malformed(`DER: an ECDSA signature has two integers, found ${parts.length}.`);
161
+ const raw = new Uint8Array(size * 2);
162
+ for (const [index, part] of parts.entries()) {
163
+ if (part.tag !== ASN1_INTEGER) throw malformed("DER: an ECDSA signature's members must be INTEGERs.");
164
+ let value = part.value;
165
+ while (value.length > 0 && value[0] === 0) value = value.subarray(1);
166
+ if (value.length > size) throw malformed(`DER: ECDSA coordinate is ${value.length} bytes, the curve is ${size}.`);
167
+ raw.set(value, index * size + (size - value.length));
168
+ }
169
+ return raw;
170
+ }
171
+
172
+ /**
173
+ * A DER BOOLEAN. DER admits exactly two encodings — `0x00` false, `0xff` true — and anything else is
174
+ * refused rather than coerced. A parser that read any non-zero byte as true would disagree with an encoder
175
+ * that wrote `0x01`, and for `basicConstraints.cA` that disagreement is the difference between a
176
+ * certificate authority and a leaf.
177
+ */
178
+ export function readBoolean(node: Asn1Node): boolean {
179
+ if (node.tag !== ASN1_BOOLEAN) throw malformed(`DER: expected a BOOLEAN, found tag 0x${node.tag.toString(16)}.`);
180
+ if (node.value.length !== 1) throw malformed(`DER: a BOOLEAN is one byte, found ${node.value.length}.`);
181
+ const byte = node.value[0] as number;
182
+ if (byte === 0x00) return false;
183
+ if (byte === 0xff) return true;
184
+ throw malformed(`DER: 0x${byte.toString(16)} is not a DER BOOLEAN; only 0x00 and 0xff are.`);
185
+ }
186
+
187
+ /**
188
+ * A non-negative INTEGER, as a number. Only used for small constrained values (`pathLenConstraint`), so a
189
+ * value wider than is plausible is refused rather than silently losing precision past 2^53.
190
+ */
191
+ export function readInteger(node: Asn1Node): number {
192
+ if (node.tag !== ASN1_INTEGER) throw malformed(`DER: expected an INTEGER, found tag 0x${node.tag.toString(16)}.`);
193
+ if (node.value.length === 0) throw malformed("DER: empty INTEGER.");
194
+ if ((node.value[0] as number) & 0x80) throw malformed("DER: a negative INTEGER is not accepted here.");
195
+ if (node.value.length > 4) throw malformed(`DER: INTEGER of ${node.value.length} bytes is wider than expected.`);
196
+ let value = 0;
197
+ for (const byte of node.value) value = value * 256 + byte;
198
+ return value;
199
+ }
200
+
201
+ /**
202
+ * A BIT STRING's bits, most-significant first, as booleans. The first contents byte counts the unused
203
+ * trailing bits of the last byte, and those are dropped — a `keyUsage` with padding read as data would
204
+ * report permissions the certificate never granted.
205
+ */
206
+ export function readBits(node: Asn1Node): boolean[] {
207
+ if (node.tag !== ASN1_BIT_STRING)
208
+ throw malformed(`DER: expected a BIT STRING, found tag 0x${node.tag.toString(16)}.`);
209
+ if (node.value.length === 0) throw malformed("DER: empty BIT STRING.");
210
+ const unused = node.value[0] as number;
211
+ if (unused > 7) throw malformed(`DER: a BIT STRING cannot have ${unused} unused bits.`);
212
+ const bytes = node.value.subarray(1);
213
+ if (bytes.length === 0) return [];
214
+ const bits: boolean[] = [];
215
+ for (const [index, byte] of bytes.entries()) {
216
+ const width = index === bytes.length - 1 ? 8 - unused : 8;
217
+ for (let bit = 0; bit < width; bit += 1) bits.push((byte & (0x80 >> bit)) !== 0);
218
+ }
219
+ return bits;
220
+ }
221
+
222
+ /** Bytes from a base64 string, standard alphabet — how a JWS header's `x5c` carries a certificate. */
223
+ export function decodeBase64(encoded: string): Uint8Array {
224
+ if (encoded.length === 0) throw malformed("base64: empty value.");
225
+ let binary: string;
226
+ try {
227
+ binary = atob(encoded);
228
+ } catch (cause) {
229
+ throw new PaymentsInvalidReceiptError({ detail: "base64: value is not valid base64." }, { cause });
230
+ }
231
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
232
+ }
233
+
234
+ /**
235
+ * Bytes from a base64url string — how a JWS carries its header, payload, and signature. The URL alphabet
236
+ * substitutes `-` and `_`, and drops the padding, so both are restored before decoding.
237
+ */
238
+ export function decodeBase64Url(encoded: string): Uint8Array {
239
+ if (encoded.length === 0) throw malformed("base64url: empty value.");
240
+ const standard = encoded.replaceAll("-", "+").replaceAll("_", "/");
241
+ return decodeBase64(standard.padEnd(standard.length + ((4 - (standard.length % 4)) % 4), "="));
242
+ }
243
+
244
+ /** Whether two byte strings have identical contents. Used to match a pinned root and to link a chain. */
245
+ export function bytesEqual(left: Uint8Array, right: Uint8Array): boolean {
246
+ if (left.length !== right.length) return false;
247
+ for (let i = 0; i < left.length; i += 1) if (left[i] !== right[i]) return false;
248
+ return true;
249
+ }