better-ship 0.39.0 → 0.39.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,7 +21,7 @@ export declare class AppleClientSecretError extends AppError {
21
21
  /**
22
22
  * Sign Apple's six-month client secret synchronously, so Better Auth options can be built without awaiting it.
23
23
  * Signing uses `node:crypto` as Typist does, so the Worker needs the `nodejs_compat` flag.
24
- * Invalid private keys return an empty secret and log once per isolate, as in Typist.
24
+ * Invalid private keys return an empty secret and log once per key ID in each isolate, as in Typist.
25
25
  */
26
26
  export declare function generateAppleClientSecret(config: AppleKeyConfig): string;
27
27
  //#endregion
@@ -4,7 +4,7 @@ import { createPrivateKey, sign } from "node:crypto";
4
4
 
5
5
  //#region src/infrastructure/better-auth/apple-client-secret.ts
6
6
  const logger = createLogger("auth");
7
- let appleKeyFailureLogged = false;
7
+ const loggedInvalidKeyIds = /* @__PURE__ */ new Set();
8
8
  /**
9
9
  * Apple caps `exp` at 15,777,000 seconds after `iat`; 180 days stays under it. Source:
10
10
  * https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret
@@ -29,15 +29,15 @@ function base64UrlJson(segment) {
29
29
  /**
30
30
  * Sign Apple's six-month client secret synchronously, so Better Auth options can be built without awaiting it.
31
31
  * Signing uses `node:crypto` as Typist does, so the Worker needs the `nodejs_compat` flag.
32
- * Invalid private keys return an empty secret and log once per isolate, as in Typist.
32
+ * Invalid private keys return an empty secret and log once per key ID in each isolate, as in Typist.
33
33
  */
34
34
  function generateAppleClientSecret(config) {
35
35
  const key = config.applePrivateKey.replace(/\\n/g, "\n");
36
36
  try {
37
37
  createPrivateKey(key);
38
38
  } catch (error) {
39
- if (!appleKeyFailureLogged) {
40
- appleKeyFailureLogged = true;
39
+ if (!loggedInvalidKeyIds.has(config.appleKeyId)) {
40
+ loggedInvalidKeyIds.add(config.appleKeyId);
41
41
  logger.error("apple_private_key_invalid", {
42
42
  error,
43
43
  keyId: config.appleKeyId
@@ -1 +1 @@
1
- {"version":3,"file":"apple-client-secret.js","names":[],"sources":["../../src/infrastructure/better-auth/apple-client-secret.ts"],"sourcesContent":["import { Buffer } from 'node:buffer'\nimport { createPrivateKey, sign } from 'node:crypto'\n\nimport { AppError, createLogger } from '@/core'\n\nconst logger = createLogger('auth')\n// Typist reports an invalid key once per isolate because session checks repeatedly construct auth options.\nlet appleKeyFailureLogged = false\n/**\n * Apple caps `exp` at 15,777,000 seconds after `iat`; 180 days stays under it. Source:\n * https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret\n */\nconst SECRET_LIFETIME_SECONDS = 180 * 24 * 60 * 60\n\n/**\n * Apple's issuer and OAuth callback origin, and the `aud` claim of the client secret. Include it in\n * trustedOrigins when enabling Apple. Source: the same Apple page as `SECRET_LIFETIME_SECONDS`.\n */\nexport const APPLE_ORIGIN = 'https://appleid.apple.com'\n\n/** Apple credentials used to sign a client secret. Ported from Porte's apple-client-secret.ts. */\nexport type AppleKeyConfig = {\n readonly appleClientId: string\n readonly appleTeamId: string\n readonly appleKeyId: string\n readonly applePrivateKey: string\n}\n\n/** Apple signing failed. The application's error boundary owns reporting. */\nexport class AppleClientSecretError extends AppError {\n readonly _tag = 'AppleClientSecretError'\n readonly classification = 'terminal'\n\n constructor(cause: unknown) {\n super('Apple client secret signing failed', { cause })\n }\n}\n\n/** One JWS segment before encoding: the protected header or the claims. */\ntype JwsSegment = Readonly<Record<string, string | number>>\n\nfunction base64UrlJson(segment: JwsSegment): string {\n return Buffer.from(JSON.stringify(segment)).toString('base64url')\n}\n\n/**\n * Sign Apple's six-month client secret synchronously, so Better Auth options can be built without awaiting it.\n * Signing uses `node:crypto` as Typist does, so the Worker needs the `nodejs_compat` flag.\n * Invalid private keys return an empty secret and log once per isolate, as in Typist.\n */\nexport function generateAppleClientSecret(config: AppleKeyConfig): string {\n const key = config.applePrivateKey.replace(/\\\\n/g, '\\n')\n // Validation only: workerd's sign() takes the PEM string, not the KeyObject.\n try {\n createPrivateKey(key)\n } catch (error) {\n if (!appleKeyFailureLogged) {\n appleKeyFailureLogged = true\n logger.error('apple_private_key_invalid', { error, keyId: config.appleKeyId })\n }\n return ''\n }\n const issuedAt = Math.floor(Date.now() / 1000)\n const header = base64UrlJson({ alg: 'ES256', kid: config.appleKeyId })\n const claims = base64UrlJson({\n iss: config.appleTeamId,\n sub: config.appleClientId,\n aud: APPLE_ORIGIN,\n iat: issuedAt,\n exp: issuedAt + SECRET_LIFETIME_SECONDS,\n })\n const input = `${header}.${claims}`\n try {\n // A JWS carries the raw r||s signature; OpenSSL emits DER unless told otherwise.\n const signature = sign('sha256', Buffer.from(input), { key, dsaEncoding: 'ieee-p1363' })\n // TypeScript 7 types the returned buffer without an encoding parameter on toString.\n return `${input}.${Buffer.from(signature).toString('base64url')}`\n } catch (cause) {\n throw new AppleClientSecretError(cause)\n }\n}\n"],"mappings":";;;;;AAKA,MAAM,SAAS,aAAa,MAAM;AAElC,IAAI,wBAAwB;;;;;AAK5B,MAAM,0BAA0B;;;;;AAMhC,MAAa,eAAe;;AAW5B,IAAa,yBAAb,cAA4C,SAAS;CACnD,AAAS,OAAO;CAChB,AAAS,iBAAiB;CAE1B,YAAY,OAAgB;EAC1B,MAAM,sCAAsC,EAAE,MAAM,CAAC;CACvD;AACF;AAKA,SAAS,cAAc,SAA6B;CAClD,OAAO,OAAO,KAAK,KAAK,UAAU,OAAO,CAAC,CAAC,CAAC,SAAS,WAAW;AAClE;;;;;;AAOA,SAAgB,0BAA0B,QAAgC;CACxE,MAAM,MAAM,OAAO,gBAAgB,QAAQ,QAAQ,IAAI;CAEvD,IAAI;EACF,iBAAiB,GAAG;CACtB,SAAS,OAAO;EACd,IAAI,CAAC,uBAAuB;GAC1B,wBAAwB;GACxB,OAAO,MAAM,6BAA6B;IAAE;IAAO,OAAO,OAAO;GAAW,CAAC;EAC/E;EACA,OAAO;CACT;CACA,MAAM,WAAW,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;CAS7C,MAAM,QAAQ,GARC,cAAc;EAAE,KAAK;EAAS,KAAK,OAAO;CAAW,CAQ9C,EAAE,GAPT,cAAc;EAC3B,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK;EACL,KAAK;EACL,KAAK,WAAW;CAClB,CACgC;CAChC,IAAI;EAEF,MAAM,YAAY,KAAK,UAAU,OAAO,KAAK,KAAK,GAAG;GAAE;GAAK,aAAa;EAAa,CAAC;EAEvF,OAAO,GAAG,MAAM,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC,SAAS,WAAW;CAChE,SAAS,OAAO;EACd,MAAM,IAAI,uBAAuB,KAAK;CACxC;AACF"}
1
+ {"version":3,"file":"apple-client-secret.js","names":[],"sources":["../../src/infrastructure/better-auth/apple-client-secret.ts"],"sourcesContent":["import { Buffer } from 'node:buffer'\nimport { createPrivateKey, sign } from 'node:crypto'\n\nimport { AppError, createLogger } from '@/core'\n\nconst logger = createLogger('auth')\n// Typist reports an invalid key once per isolate because session checks repeatedly construct auth options.\nconst loggedInvalidKeyIds = new Set<string>()\n/**\n * Apple caps `exp` at 15,777,000 seconds after `iat`; 180 days stays under it. Source:\n * https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret\n */\nconst SECRET_LIFETIME_SECONDS = 180 * 24 * 60 * 60\n\n/**\n * Apple's issuer and OAuth callback origin, and the `aud` claim of the client secret. Include it in\n * trustedOrigins when enabling Apple. Source: the same Apple page as `SECRET_LIFETIME_SECONDS`.\n */\nexport const APPLE_ORIGIN = 'https://appleid.apple.com'\n\n/** Apple credentials used to sign a client secret. Ported from Porte's apple-client-secret.ts. */\nexport type AppleKeyConfig = {\n readonly appleClientId: string\n readonly appleTeamId: string\n readonly appleKeyId: string\n readonly applePrivateKey: string\n}\n\n/** Apple signing failed. The application's error boundary owns reporting. */\nexport class AppleClientSecretError extends AppError {\n readonly _tag = 'AppleClientSecretError'\n readonly classification = 'terminal'\n\n constructor(cause: unknown) {\n super('Apple client secret signing failed', { cause })\n }\n}\n\n/** One JWS segment before encoding: the protected header or the claims. */\ntype JwsSegment = Readonly<Record<string, string | number>>\n\nfunction base64UrlJson(segment: JwsSegment): string {\n return Buffer.from(JSON.stringify(segment)).toString('base64url')\n}\n\n/**\n * Sign Apple's six-month client secret synchronously, so Better Auth options can be built without awaiting it.\n * Signing uses `node:crypto` as Typist does, so the Worker needs the `nodejs_compat` flag.\n * Invalid private keys return an empty secret and log once per key ID in each isolate, as in Typist.\n */\nexport function generateAppleClientSecret(config: AppleKeyConfig): string {\n const key = config.applePrivateKey.replace(/\\\\n/g, '\\n')\n // Validation only: workerd's sign() takes the PEM string, not the KeyObject.\n try {\n createPrivateKey(key)\n } catch (error) {\n if (!loggedInvalidKeyIds.has(config.appleKeyId)) {\n loggedInvalidKeyIds.add(config.appleKeyId)\n logger.error('apple_private_key_invalid', { error, keyId: config.appleKeyId })\n }\n return ''\n }\n const issuedAt = Math.floor(Date.now() / 1000)\n const header = base64UrlJson({ alg: 'ES256', kid: config.appleKeyId })\n const claims = base64UrlJson({\n iss: config.appleTeamId,\n sub: config.appleClientId,\n aud: APPLE_ORIGIN,\n iat: issuedAt,\n exp: issuedAt + SECRET_LIFETIME_SECONDS,\n })\n const input = `${header}.${claims}`\n try {\n // A JWS carries the raw r||s signature; OpenSSL emits DER unless told otherwise.\n const signature = sign('sha256', Buffer.from(input), { key, dsaEncoding: 'ieee-p1363' })\n // TypeScript 7 types the returned buffer without an encoding parameter on toString.\n return `${input}.${Buffer.from(signature).toString('base64url')}`\n } catch (cause) {\n throw new AppleClientSecretError(cause)\n }\n}\n"],"mappings":";;;;;AAKA,MAAM,SAAS,aAAa,MAAM;AAElC,MAAM,sCAAsB,IAAI,IAAY;;;;;AAK5C,MAAM,0BAA0B;;;;;AAMhC,MAAa,eAAe;;AAW5B,IAAa,yBAAb,cAA4C,SAAS;CACnD,AAAS,OAAO;CAChB,AAAS,iBAAiB;CAE1B,YAAY,OAAgB;EAC1B,MAAM,sCAAsC,EAAE,MAAM,CAAC;CACvD;AACF;AAKA,SAAS,cAAc,SAA6B;CAClD,OAAO,OAAO,KAAK,KAAK,UAAU,OAAO,CAAC,CAAC,CAAC,SAAS,WAAW;AAClE;;;;;;AAOA,SAAgB,0BAA0B,QAAgC;CACxE,MAAM,MAAM,OAAO,gBAAgB,QAAQ,QAAQ,IAAI;CAEvD,IAAI;EACF,iBAAiB,GAAG;CACtB,SAAS,OAAO;EACd,IAAI,CAAC,oBAAoB,IAAI,OAAO,UAAU,GAAG;GAC/C,oBAAoB,IAAI,OAAO,UAAU;GACzC,OAAO,MAAM,6BAA6B;IAAE;IAAO,OAAO,OAAO;GAAW,CAAC;EAC/E;EACA,OAAO;CACT;CACA,MAAM,WAAW,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;CAS7C,MAAM,QAAQ,GARC,cAAc;EAAE,KAAK;EAAS,KAAK,OAAO;CAAW,CAQ9C,EAAE,GAPT,cAAc;EAC3B,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK;EACL,KAAK;EACL,KAAK,WAAW;CAClB,CACgC;CAChC,IAAI;EAEF,MAAM,YAAY,KAAK,UAAU,OAAO,KAAK,KAAK,GAAG;GAAE;GAAK,aAAa;EAAa,CAAC;EAEvF,OAAO,GAAG,MAAM,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC,SAAS,WAAW;CAChE,SAAS,OAAO;EACd,MAAM,IAAI,uBAAuB,KAAK;CACxC;AACF"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://www.schemastore.org/package.json",
3
3
  "name": "better-ship",
4
- "version": "0.39.0",
4
+ "version": "0.39.1",
5
5
  "description": "Foundation for applications on Cloudflare Workers: logger, errors, message bus, infrastructure adapters, and UI primitives",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -86,6 +86,7 @@
86
86
  "@better-auth/stripe": "^1.7.10",
87
87
  "@cloudflare/vitest-plugin": "^1.4.4",
88
88
  "@cloudflare/workers-types": "5.20261012.1",
89
+ "@fast-check/vitest": "^0.5.0",
89
90
  "@mdn/browser-compat-data": "^8.1.6",
90
91
  "@oxlint/plugins": "^1.88.0",
91
92
  "@posthog/react": "^1.11.3",
@@ -102,11 +103,14 @@
102
103
  "@types/pg": "^8.23.1",
103
104
  "@types/react": "^19.3.0",
104
105
  "@vitest/browser-playwright": "^5.0.5",
106
+ "@vitest/coverage-istanbul": "^5.0.5",
105
107
  "auth": "^1.7.10",
106
108
  "better-auth": "^1.7.10",
107
109
  "browserslist": "^4.29.3",
108
110
  "drizzle-kit": "^0.31.11",
109
111
  "drizzle-orm": "^0.45.4",
112
+ "fast-check": "^4.10.2",
113
+ "msw": "^3.0.3",
110
114
  "pg": "^8.23.1",
111
115
  "playwright": "^1.64.1",
112
116
  "posthog-js": "^1.438.12",
@@ -202,6 +206,8 @@
202
206
  "lint": "oxlint . --quiet",
203
207
  "lint:fix": "oxlint . --fix --quiet",
204
208
  "test": "vitest run",
209
+ "test:live": "TEST_LIVE=1 vitest run --tags-filter=live",
210
+ "test:coverage": "vitest run --coverage.enabled",
205
211
  "cf-typegen": "wrangler types tests/infrastructure/cloudflare/worker-configuration.d.ts --config tests/infrastructure/cloudflare/wrangler.jsonc --include-runtime=false",
206
212
  "db:generate:d1": "drizzle-kit generate --config drizzle.d1.config.ts",
207
213
  "db:generate:postgres": "drizzle-kit generate --config drizzle.postgres.config.ts"
@@ -5,7 +5,7 @@ import { AppError, createLogger } from '@/core'
5
5
 
6
6
  const logger = createLogger('auth')
7
7
  // Typist reports an invalid key once per isolate because session checks repeatedly construct auth options.
8
- let appleKeyFailureLogged = false
8
+ const loggedInvalidKeyIds = new Set<string>()
9
9
  /**
10
10
  * Apple caps `exp` at 15,777,000 seconds after `iat`; 180 days stays under it. Source:
11
11
  * https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret
@@ -46,7 +46,7 @@ function base64UrlJson(segment: JwsSegment): string {
46
46
  /**
47
47
  * Sign Apple's six-month client secret synchronously, so Better Auth options can be built without awaiting it.
48
48
  * Signing uses `node:crypto` as Typist does, so the Worker needs the `nodejs_compat` flag.
49
- * Invalid private keys return an empty secret and log once per isolate, as in Typist.
49
+ * Invalid private keys return an empty secret and log once per key ID in each isolate, as in Typist.
50
50
  */
51
51
  export function generateAppleClientSecret(config: AppleKeyConfig): string {
52
52
  const key = config.applePrivateKey.replace(/\\n/g, '\n')
@@ -54,8 +54,8 @@ export function generateAppleClientSecret(config: AppleKeyConfig): string {
54
54
  try {
55
55
  createPrivateKey(key)
56
56
  } catch (error) {
57
- if (!appleKeyFailureLogged) {
58
- appleKeyFailureLogged = true
57
+ if (!loggedInvalidKeyIds.has(config.appleKeyId)) {
58
+ loggedInvalidKeyIds.add(config.appleKeyId)
59
59
  logger.error('apple_private_key_invalid', { error, keyId: config.appleKeyId })
60
60
  }
61
61
  return ''