ras-stack 0.45.0 → 0.46.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.
- package/README.md +2 -2
- package/dist/auth/client.d.ts +3 -0
- package/dist/auth/client.js +40 -0
- package/dist/auth/client.js.map +1 -1
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/index.js +1 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/settings.d.ts +13 -1
- package/dist/auth/settings.js +8 -0
- package/dist/auth/settings.js.map +1 -1
- package/dist/conformance/index.js +6 -0
- package/dist/conformance/index.js.map +1 -1
- package/dist/email/index.d.ts +19 -0
- package/dist/email/index.js +3 -0
- package/dist/email/index.js.map +1 -1
- package/dist/posthog/client.js +2 -0
- package/dist/posthog/client.js.map +1 -1
- package/dist/posthog/react.js +21 -6
- package/dist/posthog/react.js.map +1 -1
- package/examples/full-stack/src/server/auth-flow.test.ts +2 -0
- package/examples/full-stack/src/server/auth.ts +28 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,10 +39,10 @@ These combinations are tested here and in production. [Sealed Lists](https://git
|
|
|
39
39
|
| ------------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
40
40
|
| Runtime and tooling | Node, ESM TypeScript, pnpm, Just, Oxlint | Compiler/linter bases, setup actions, and version synchronization |
|
|
41
41
|
| Web application | TanStack Start, React, TanStack Query | Request binding, mutation-origin checks, canonical hosts, health handlers, and Query defaults |
|
|
42
|
-
| Authentication | Better Auth | Secure option builders, origins, secrets,
|
|
42
|
+
| Authentication | Better Auth | Secure option builders, origins, secrets, redirects, failure classification, and React action state |
|
|
43
43
|
| Data | Drizzle, `better-sqlite3`, Postgres.js | Connection lifecycle, safety defaults, migrations, target selection, and conformance checks |
|
|
44
44
|
| Realtime | Centrifuge, Centrifugo, Caddy | Publishing, tokens, browser/React lifecycle, presence, proxy configuration, binaries, and supervision |
|
|
45
|
-
| Email and uploads | Nodemailer, `tus-js-client` | SMTP configuration/delivery and promise-based resumable uploads
|
|
45
|
+
| Email and uploads | Nodemailer, `tus-js-client` | SMTP configuration/delivery, auth callbacks, and promise-based resumable uploads |
|
|
46
46
|
| Observability | PostHog JS, React, and Node SDKs | Initialization, error defaults, request correlation, proxy routes, shutdown, and coverage decisions |
|
|
47
47
|
| Delivery | GitHub Actions, Changesets, Dokploy, Docker | Checks, releases, preview lifecycle/status, production assets, and runtime binaries |
|
|
48
48
|
|
package/dist/auth/client.d.ts
CHANGED
|
@@ -3,6 +3,9 @@ export type AuthFailure = {
|
|
|
3
3
|
code?: string;
|
|
4
4
|
message?: string;
|
|
5
5
|
} | null | undefined;
|
|
6
|
+
export type AuthCallbackFailureReason = 'account_already_linked' | 'account_not_linked' | 'email_mismatch' | 'error' | 'invalid_token' | 'token_expired';
|
|
6
7
|
export type SignInFailureReason = 'invalid_credentials' | 'rate_limited' | 'error';
|
|
8
|
+
export declare function classifyAuthCallbackFailure(error: unknown): AuthCallbackFailureReason;
|
|
7
9
|
export declare function classifySignInFailure(failure: unknown): SignInFailureReason;
|
|
8
10
|
export declare function authFailureMessage(failure: unknown, fallback: string): string;
|
|
11
|
+
export declare function localRedirectPath(value: unknown): string | undefined;
|
package/dist/auth/client.js
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
const REDIRECT_ORIGIN = 'https://ras-stack.invalid';
|
|
2
|
+
export function classifyAuthCallbackFailure(error) {
|
|
3
|
+
if (typeof error !== 'string')
|
|
4
|
+
return 'error';
|
|
5
|
+
switch (error.trim().toLowerCase()) {
|
|
6
|
+
case 'account_not_linked':
|
|
7
|
+
case 'account not linked':
|
|
8
|
+
case 'unable_to_link_account':
|
|
9
|
+
return 'account_not_linked';
|
|
10
|
+
case "email_doesn't_match":
|
|
11
|
+
case 'email_does_not_match':
|
|
12
|
+
case 'email_mismatch':
|
|
13
|
+
return 'email_mismatch';
|
|
14
|
+
case 'account_already_linked_to_different_user':
|
|
15
|
+
case 'social_account_already_linked':
|
|
16
|
+
return 'account_already_linked';
|
|
17
|
+
case 'invalid_token':
|
|
18
|
+
return 'invalid_token';
|
|
19
|
+
case 'token_expired':
|
|
20
|
+
return 'token_expired';
|
|
21
|
+
default:
|
|
22
|
+
return 'error';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
1
25
|
export function classifySignInFailure(failure) {
|
|
2
26
|
if (!failure || typeof failure !== 'object')
|
|
3
27
|
return 'error';
|
|
@@ -14,4 +38,20 @@ export function authFailureMessage(failure, fallback) {
|
|
|
14
38
|
return fallback;
|
|
15
39
|
return typeof failure.message === 'string' && failure.message.trim() ? failure.message : fallback;
|
|
16
40
|
}
|
|
41
|
+
export function localRedirectPath(value) {
|
|
42
|
+
if (typeof value !== 'string' || !value.startsWith('/'))
|
|
43
|
+
return undefined;
|
|
44
|
+
try {
|
|
45
|
+
const resolved = new URL(value, REDIRECT_ORIGIN);
|
|
46
|
+
if (resolved.origin !== REDIRECT_ORIGIN)
|
|
47
|
+
return undefined;
|
|
48
|
+
const destination = `${resolved.pathname}${resolved.search}${resolved.hash}`;
|
|
49
|
+
if (new URL(destination, REDIRECT_ORIGIN).origin !== REDIRECT_ORIGIN)
|
|
50
|
+
return undefined;
|
|
51
|
+
return destination;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
17
57
|
//# sourceMappingURL=client.js.map
|
package/dist/auth/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/auth/client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/auth/client.ts"],"names":[],"mappings":"AAUA,MAAM,eAAe,GAAG,2BAA2B,CAAA;AAEnD,MAAM,UAAU,2BAA2B,CAAC,KAAc;IACxD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,OAAO,CAAA;IAC7C,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;QACnC,KAAK,oBAAoB,CAAC;QAC1B,KAAK,oBAAoB,CAAC;QAC1B,KAAK,wBAAwB;YAC3B,OAAO,oBAAoB,CAAA;QAC7B,KAAK,qBAAqB,CAAC;QAC3B,KAAK,sBAAsB,CAAC;QAC5B,KAAK,gBAAgB;YACnB,OAAO,gBAAgB,CAAA;QACzB,KAAK,0CAA0C,CAAC;QAChD,KAAK,+BAA+B;YAClC,OAAO,wBAAwB,CAAA;QACjC,KAAK,eAAe;YAClB,OAAO,eAAe,CAAA;QACxB,KAAK,eAAe;YAClB,OAAO,eAAe,CAAA;QACxB;YACE,OAAO,OAAO,CAAA;IAClB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,OAAgB;IACpD,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,OAAO,CAAA;IAC3D,MAAM,MAAM,GAAG,QAAQ,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAA;IAC/D,MAAM,IAAI,GAAG,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAA;IACzD,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,cAAc,CAAA;IACzC,IAAI,MAAM,KAAK,GAAG,IAAI,IAAI,KAAK,2BAA2B;QAAE,OAAO,qBAAqB,CAAA;IACxF,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,OAAgB,EAAE,QAAgB;IACnE,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,CAAC,CAAC,SAAS,IAAI,OAAO,CAAC;QAAE,OAAO,QAAQ,CAAA;IACvF,OAAO,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAA;AACnG,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC9C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAA;IACzE,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,KAAK,EAAE,eAAe,CAAC,CAAA;QAChD,IAAI,QAAQ,CAAC,MAAM,KAAK,eAAe;YAAE,OAAO,SAAS,CAAA;QACzD,MAAM,WAAW,GAAG,GAAG,QAAQ,CAAC,QAAQ,GAAG,QAAQ,CAAC,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAA;QAC5E,IAAI,IAAI,GAAG,CAAC,WAAW,EAAE,eAAe,CAAC,CAAC,MAAM,KAAK,eAAe;YAAE,OAAO,SAAS,CAAA;QACtF,OAAO,WAAW,CAAA;IACpB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC","sourcesContent":["export type AuthFailure = { status?: number; code?: string; message?: string } | null | undefined\nexport type AuthCallbackFailureReason =\n | 'account_already_linked'\n | 'account_not_linked'\n | 'email_mismatch'\n | 'error'\n | 'invalid_token'\n | 'token_expired'\nexport type SignInFailureReason = 'invalid_credentials' | 'rate_limited' | 'error'\n\nconst REDIRECT_ORIGIN = 'https://ras-stack.invalid'\n\nexport function classifyAuthCallbackFailure(error: unknown): AuthCallbackFailureReason {\n if (typeof error !== 'string') return 'error'\n switch (error.trim().toLowerCase()) {\n case 'account_not_linked':\n case 'account not linked':\n case 'unable_to_link_account':\n return 'account_not_linked'\n case \"email_doesn't_match\":\n case 'email_does_not_match':\n case 'email_mismatch':\n return 'email_mismatch'\n case 'account_already_linked_to_different_user':\n case 'social_account_already_linked':\n return 'account_already_linked'\n case 'invalid_token':\n return 'invalid_token'\n case 'token_expired':\n return 'token_expired'\n default:\n return 'error'\n }\n}\n\nexport function classifySignInFailure(failure: unknown): SignInFailureReason {\n if (!failure || typeof failure !== 'object') return 'error'\n const status = 'status' in failure ? failure.status : undefined\n const code = 'code' in failure ? failure.code : undefined\n if (status === 429) return 'rate_limited'\n if (status === 401 || code === 'INVALID_EMAIL_OR_PASSWORD') return 'invalid_credentials'\n return 'error'\n}\n\nexport function authFailureMessage(failure: unknown, fallback: string) {\n if (!failure || typeof failure !== 'object' || !('message' in failure)) return fallback\n return typeof failure.message === 'string' && failure.message.trim() ? failure.message : fallback\n}\n\nexport function localRedirectPath(value: unknown) {\n if (typeof value !== 'string' || !value.startsWith('/')) return undefined\n try {\n const resolved = new URL(value, REDIRECT_ORIGIN)\n if (resolved.origin !== REDIRECT_ORIGIN) return undefined\n const destination = `${resolved.pathname}${resolved.search}${resolved.hash}`\n if (new URL(destination, REDIRECT_ORIGIN).origin !== REDIRECT_ORIGIN) return undefined\n return destination\n } catch {\n return undefined\n }\n}\n"]}
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -5,5 +5,5 @@ export type { ProviderCredentials, ProviderEnvironmentOptions } from './provider
|
|
|
5
5
|
export { randomId, randomToken } from './random.js';
|
|
6
6
|
export { persistedSecret } from './secret.js';
|
|
7
7
|
export type { PersistedSecretOptions } from './secret.js';
|
|
8
|
-
export { standardAccountOptions, standardRateLimitOptions, standardSessionOptions } from './settings.js';
|
|
9
|
-
export type { RateLimitRule, SessionOptions, StandardAccountOptions } from './settings.js';
|
|
8
|
+
export { standardAccountOptions, standardEmailAndPasswordOptions, standardRateLimitOptions, standardSessionOptions } from './settings.js';
|
|
9
|
+
export type { RateLimitRule, SessionOptions, StandardAccountOptions, StandardEmailAndPasswordOptions } from './settings.js';
|
package/dist/auth/index.js
CHANGED
|
@@ -2,5 +2,5 @@ export { acceptedOrigins, forwardedOrigin, parseOrigin, requireSameOrigin, trust
|
|
|
2
2
|
export { configuredProviderOptions, configuredProviders, providerCredentials } from './providers.js';
|
|
3
3
|
export { randomId, randomToken } from './random.js';
|
|
4
4
|
export { persistedSecret } from './secret.js';
|
|
5
|
-
export { standardAccountOptions, standardRateLimitOptions, standardSessionOptions } from './settings.js';
|
|
5
|
+
export { standardAccountOptions, standardEmailAndPasswordOptions, standardRateLimitOptions, standardSessionOptions } from './settings.js';
|
|
6
6
|
//# sourceMappingURL=index.js.map
|
package/dist/auth/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,WAAW,EAAE,iBAAiB,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAEvI,OAAO,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AAEpG,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAE7C,OAAO,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA","sourcesContent":["export { acceptedOrigins, forwardedOrigin, parseOrigin, requireSameOrigin, trustedOrigins, validSameOriginRequest } from './origins.js'\nexport type { OriginOptions } from './origins.js'\nexport { configuredProviderOptions, configuredProviders, providerCredentials } from './providers.js'\nexport type { ProviderCredentials, ProviderEnvironmentOptions } from './providers.js'\nexport { randomId, randomToken } from './random.js'\nexport { persistedSecret } from './secret.js'\nexport type { PersistedSecretOptions } from './secret.js'\nexport { standardAccountOptions, standardRateLimitOptions, standardSessionOptions } from './settings.js'\nexport type { RateLimitRule, SessionOptions, StandardAccountOptions } from './settings.js'\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,WAAW,EAAE,iBAAiB,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAEvI,OAAO,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AAEpG,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAE7C,OAAO,EAAE,sBAAsB,EAAE,+BAA+B,EAAE,wBAAwB,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA","sourcesContent":["export { acceptedOrigins, forwardedOrigin, parseOrigin, requireSameOrigin, trustedOrigins, validSameOriginRequest } from './origins.js'\nexport type { OriginOptions } from './origins.js'\nexport { configuredProviderOptions, configuredProviders, providerCredentials } from './providers.js'\nexport type { ProviderCredentials, ProviderEnvironmentOptions } from './providers.js'\nexport { randomId, randomToken } from './random.js'\nexport { persistedSecret } from './secret.js'\nexport type { PersistedSecretOptions } from './secret.js'\nexport { standardAccountOptions, standardEmailAndPasswordOptions, standardRateLimitOptions, standardSessionOptions } from './settings.js'\nexport type { RateLimitRule, SessionOptions, StandardAccountOptions, StandardEmailAndPasswordOptions } from './settings.js'\n"]}
|
package/dist/auth/settings.d.ts
CHANGED
|
@@ -9,13 +9,21 @@ export type SessionOptions = {
|
|
|
9
9
|
export type StandardAccountOptions = {
|
|
10
10
|
encryptOAuthTokens?: boolean;
|
|
11
11
|
};
|
|
12
|
+
export type StandardEmailAndPasswordOptions = {
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
revokeSessionsOnPasswordReset?: boolean;
|
|
15
|
+
};
|
|
12
16
|
export declare function standardSessionOptions<const Options extends object>(overrides?: Options & SessionOptions): Options & {
|
|
13
17
|
expiresIn: number;
|
|
14
18
|
updateAge: number;
|
|
15
19
|
};
|
|
16
|
-
export declare function standardAccountOptions<
|
|
20
|
+
export declare function standardAccountOptions<Options extends object>(overrides?: Options & StandardAccountOptions): Options & {
|
|
17
21
|
encryptOAuthTokens: boolean;
|
|
18
22
|
};
|
|
23
|
+
export declare function standardEmailAndPasswordOptions<Options extends object>(overrides?: Options & StandardEmailAndPasswordOptions): Options & {
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
revokeSessionsOnPasswordReset: boolean;
|
|
26
|
+
};
|
|
19
27
|
export declare function standardRateLimitOptions(customRules?: Record<string, RateLimitRule>): {
|
|
20
28
|
enabled: boolean;
|
|
21
29
|
storage: 'database';
|
|
@@ -34,6 +42,10 @@ export declare function standardRateLimitOptions(customRules?: Record<string, Ra
|
|
|
34
42
|
window: number;
|
|
35
43
|
max: number;
|
|
36
44
|
};
|
|
45
|
+
'/send-verification-email': {
|
|
46
|
+
window: number;
|
|
47
|
+
max: number;
|
|
48
|
+
};
|
|
37
49
|
'/admin/set-user-password': {
|
|
38
50
|
window: number;
|
|
39
51
|
max: number;
|
package/dist/auth/settings.js
CHANGED
|
@@ -11,6 +11,13 @@ export function standardAccountOptions(overrides = {}) {
|
|
|
11
11
|
encryptOAuthTokens: overrides.encryptOAuthTokens ?? true,
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
|
+
export function standardEmailAndPasswordOptions(overrides = {}) {
|
|
15
|
+
return {
|
|
16
|
+
...overrides,
|
|
17
|
+
enabled: overrides.enabled ?? true,
|
|
18
|
+
revokeSessionsOnPasswordReset: overrides.revokeSessionsOnPasswordReset ?? true,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
14
21
|
export function standardRateLimitOptions(customRules = {}) {
|
|
15
22
|
return {
|
|
16
23
|
enabled: true,
|
|
@@ -21,6 +28,7 @@ export function standardRateLimitOptions(customRules = {}) {
|
|
|
21
28
|
'/sign-in/email': { window: 60, max: 20 },
|
|
22
29
|
'/sign-up/email': { window: 60, max: 15 },
|
|
23
30
|
'/request-password-reset': { window: 60, max: 5 },
|
|
31
|
+
'/send-verification-email': { window: 60, max: 5 },
|
|
24
32
|
'/admin/set-user-password': { window: 60, max: 10 },
|
|
25
33
|
...customRules,
|
|
26
34
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/auth/settings.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/auth/settings.ts"],"names":[],"mappings":"AAMA,MAAM,UAAU,sBAAsB,CAA+B,SAAS,GAA6B,EAA8B;IACvI,OAAO;QACL,GAAG,SAAS;QACZ,SAAS,EAAE,SAAS,CAAC,SAAS,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE;QACnD,SAAS,EAAE,SAAS,CAAC,SAAS,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE;KAC/C,CAAA;AACH,CAAC;AAED,MAAM,UAAU,sBAAsB,CACpC,SAAS,GAAqC,EAAsC;IAEpF,OAAO;QACL,GAAG,SAAS;QACZ,kBAAkB,EAAE,SAAS,CAAC,kBAAkB,IAAI,IAAI;KACzD,CAAA;AACH,CAAC;AAED,MAAM,UAAU,+BAA+B,CAC7C,SAAS,GAA8C,EAA+C;IAEtG,OAAO;QACL,GAAG,SAAS;QACZ,OAAO,EAAE,SAAS,CAAC,OAAO,IAAI,IAAI;QAClC,6BAA6B,EAAE,SAAS,CAAC,6BAA6B,IAAI,IAAI;KAC/E,CAAA;AACH,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,WAAW,GAAkC,EAAE;IACtF,OAAO;QACL,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,UAAmB;QAC5B,MAAM,EAAE,EAAE;QACV,GAAG,EAAE,GAAG;QACR,WAAW,EAAE;YACX,gBAAgB,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;YACzC,gBAAgB,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;YACzC,yBAAyB,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE;YACjD,0BAA0B,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE;YAClD,0BAA0B,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;YACnD,GAAG,WAAW;SACf;KACF,CAAA;AACH,CAAC","sourcesContent":["export type RateLimitRule = { window: number; max: number }\n\nexport type SessionOptions = { expiresIn?: number | undefined; updateAge?: number | undefined }\nexport type StandardAccountOptions = { encryptOAuthTokens?: boolean }\nexport type StandardEmailAndPasswordOptions = { enabled?: boolean; revokeSessionsOnPasswordReset?: boolean }\n\nexport function standardSessionOptions<const Options extends object>(overrides: Options & SessionOptions = {} as Options & SessionOptions) {\n return {\n ...overrides,\n expiresIn: overrides.expiresIn ?? 60 * 60 * 24 * 90,\n updateAge: overrides.updateAge ?? 60 * 60 * 24,\n }\n}\n\nexport function standardAccountOptions<Options extends object>(\n overrides: Options & StandardAccountOptions = {} as Options & StandardAccountOptions,\n) {\n return {\n ...overrides,\n encryptOAuthTokens: overrides.encryptOAuthTokens ?? true,\n }\n}\n\nexport function standardEmailAndPasswordOptions<Options extends object>(\n overrides: Options & StandardEmailAndPasswordOptions = {} as Options & StandardEmailAndPasswordOptions,\n) {\n return {\n ...overrides,\n enabled: overrides.enabled ?? true,\n revokeSessionsOnPasswordReset: overrides.revokeSessionsOnPasswordReset ?? true,\n }\n}\n\nexport function standardRateLimitOptions(customRules: Record<string, RateLimitRule> = {}) {\n return {\n enabled: true,\n storage: 'database' as const,\n window: 60,\n max: 120,\n customRules: {\n '/sign-in/email': { window: 60, max: 20 },\n '/sign-up/email': { window: 60, max: 15 },\n '/request-password-reset': { window: 60, max: 5 },\n '/send-verification-email': { window: 60, max: 5 },\n '/admin/set-user-password': { window: 60, max: 10 },\n ...customRules,\n },\n }\n}\n"]}
|
|
@@ -269,6 +269,12 @@ export function assertPostHogBrowserConformance(options) {
|
|
|
269
269
|
if (options.person_profiles !== 'identified_only') {
|
|
270
270
|
throw new ConformanceError('PostHog browser initialization', 'person profiles must be limited to identified users');
|
|
271
271
|
}
|
|
272
|
+
if (options.mask_personal_data_properties !== true) {
|
|
273
|
+
throw new ConformanceError('PostHog browser initialization', 'personal-data URL properties must be masked');
|
|
274
|
+
}
|
|
275
|
+
if (!Array.isArray(options.custom_personal_data_properties) || !options.custom_personal_data_properties.includes('token')) {
|
|
276
|
+
throw new ConformanceError('PostHog browser initialization', 'token query parameters must be masked');
|
|
277
|
+
}
|
|
272
278
|
const recording = options.session_recording;
|
|
273
279
|
if (!recording || typeof recording !== 'object' || !('maskAllInputs' in recording) || recording.maskAllInputs !== true) {
|
|
274
280
|
throw new ConformanceError('PostHog browser initialization', 'session replay must mask all inputs by default');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/conformance/index.ts"],"names":[],"mappings":"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","sourcesContent":["export class ConformanceError extends Error {\n constructor(\n readonly scenario: string,\n message: string,\n options: { cause?: unknown } = {},\n ) {\n super(`${scenario}: ${message}`, options.cause === undefined ? undefined : { cause: options.cause })\n this.name = 'ConformanceError'\n }\n}\n\nexport async function assertMutationOriginConformance(\n guard: (request: Request) => void | Promise<void>,\n options: { trustForwardedHeaders?: boolean } = {},\n) {\n await accepted('same-origin request', () =>\n guard(\n new Request('https://app.example/action', {\n method: 'POST',\n headers: { origin: 'https://app.example', 'sec-fetch-site': 'same-origin' },\n }),\n ),\n )\n await rejected('cross-origin request', () =>\n guard(\n new Request('https://app.example/action', {\n method: 'POST',\n headers: { origin: 'https://attacker.example', 'sec-fetch-site': 'cross-site' },\n }),\n ),\n )\n await rejected('missing-origin request', () => guard(new Request('https://app.example/action', { method: 'POST' })))\n if (!options.trustForwardedHeaders) {\n await rejected('spoofed forwarded-origin request', () =>\n guard(\n new Request('https://app.example/action', {\n method: 'POST',\n headers: {\n origin: 'https://attacker.example',\n 'x-forwarded-host': 'attacker.example',\n 'x-forwarded-proto': 'https',\n },\n }),\n ),\n )\n }\n}\n\nexport type HealthHandlerFactory = (check: () => void | Promise<void>) => () => Response | Promise<Response>\n\nexport async function assertHealthHandlerConformance(createHandler: HealthHandlerFactory) {\n const healthy = await createHandler(() => undefined)()\n if (healthy.status !== 200) throw new ConformanceError('healthy dependency', `expected status 200, received ${healthy.status}`)\n const healthyBody = await responseBody(healthy, 'healthy dependency')\n if (healthyBody.ok !== true) throw new ConformanceError('healthy dependency', 'response body must contain ok: true')\n\n const privateMessage = 'password=private-diagnostic'\n const unavailable = await createHandler(() => Promise.reject(new Error(privateMessage)))()\n if (unavailable.status !== 503) {\n throw new ConformanceError('unavailable dependency', `expected status 503, received ${unavailable.status}`)\n }\n const unavailableText = await unavailable.text()\n if (unavailableText.includes(privateMessage)) {\n throw new ConformanceError('unavailable dependency', 'response exposed the private diagnostic message')\n }\n let unavailableBody: unknown\n try {\n unavailableBody = JSON.parse(unavailableText)\n } catch (error) {\n throw new ConformanceError('unavailable dependency', 'response body must be JSON', { cause: error })\n }\n if (!unavailableBody || typeof unavailableBody !== 'object' || !('ok' in unavailableBody) || unavailableBody.ok !== false) {\n throw new ConformanceError('unavailable dependency', 'response body must contain ok: false')\n }\n}\n\nexport type SqlitePragmaReader = (name: 'journal_mode' | 'synchronous' | 'busy_timeout' | 'foreign_keys') => unknown\n\nexport async function assertSqliteConformance(readPragma: SqlitePragmaReader) {\n const values = {\n journalMode: String(await readPragma('journal_mode')).toLowerCase(),\n synchronous: Number(await readPragma('synchronous')),\n busyTimeout: Number(await readPragma('busy_timeout')),\n foreignKeys: Number(await readPragma('foreign_keys')),\n }\n if (values.journalMode !== 'wal' && values.journalMode !== 'memory') {\n throw new ConformanceError('SQLite journal mode', `expected wal or memory, received ${values.journalMode}`)\n }\n if (values.synchronous !== 2) {\n throw new ConformanceError('SQLite synchronous mode', `expected FULL (2), received ${values.synchronous}`)\n }\n if (values.busyTimeout !== 5000) {\n throw new ConformanceError('SQLite busy timeout', `expected 5000, received ${values.busyTimeout}`)\n }\n if (values.foreignKeys !== 1) {\n throw new ConformanceError('SQLite foreign keys', `expected enabled (1), received ${values.foreignKeys}`)\n }\n}\n\nexport type RealtimeTokenSigner = (subject: string, claims: Record<string, unknown>) => string | Promise<string>\n\nexport type RealtimeTokenConformanceOptions = { secret: string; maxTtlSeconds?: number; now?: number }\n\nexport async function assertRealtimeTokenConformance(sign: RealtimeTokenSigner, options: RealtimeTokenConformanceOptions) {\n const now = options.now ?? Math.floor(Date.now() / 1000)\n const maxTtlSeconds = options.maxTtlSeconds ?? 60 * 60\n const token = await sign('person-123', { channel: 'room:1' })\n const { header, payload, signed, signature } = decodeToken(token)\n\n if (header.alg !== 'HS256') {\n throw new ConformanceError('realtime token algorithm', `expected HS256, received ${JSON.stringify(header.alg)}`)\n }\n if (payload.sub !== 'person-123') {\n throw new ConformanceError('realtime token subject', 'token must bind the subject it was signed for')\n }\n if (payload.channel !== 'room:1') {\n throw new ConformanceError('realtime token claims', 'token must carry the claims it was signed with')\n }\n if (typeof payload.exp !== 'number') {\n throw new ConformanceError('realtime token expiry', 'token must expire')\n }\n if (payload.exp <= now) {\n throw new ConformanceError('realtime token expiry', 'token expired before it was issued')\n }\n if (payload.exp - now > maxTtlSeconds) {\n throw new ConformanceError('realtime token expiry', `token outlives the ${maxTtlSeconds} second maximum`)\n }\n if (!(await verifyHmac(signed, signature, options.secret))) {\n throw new ConformanceError('realtime token signature', 'token is not signed with the shared Centrifugo secret')\n }\n\n const other = decodeToken(await sign('person-456', { channel: 'room:1' })).payload\n if (other.sub === payload.sub) {\n throw new ConformanceError('realtime token subject', 'every subject received the same identity')\n }\n}\n\nfunction decodeToken(token: string) {\n const segments = token.split('.')\n if (segments.length !== 3) throw new ConformanceError('realtime token format', 'expected a three-segment JWT')\n const [header, claims, signature] = segments as [string, string, string]\n return {\n header: decodeSegment(header, 'header'),\n payload: decodeSegment(claims, 'payload'),\n signed: `${header}.${claims}`,\n signature,\n }\n}\n\nfunction decodeSegment(segment: string, name: string): Record<string, unknown> {\n try {\n const padded = segment\n .replaceAll('-', '+')\n .replaceAll('_', '/')\n .padEnd(Math.ceil(segment.length / 4) * 4, '=')\n const value: unknown = JSON.parse(atob(padded))\n if (value && typeof value === 'object') return value as Record<string, unknown>\n } catch (error) {\n throw new ConformanceError('realtime token format', `${name} is not base64url JSON`, { cause: error })\n }\n throw new ConformanceError('realtime token format', `${name} is not an object`)\n}\n\nasync function verifyHmac(signed: string, signature: string, secret: string) {\n const encoder = new TextEncoder()\n const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])\n const digest = new Uint8Array(await crypto.subtle.sign('HMAC', key, encoder.encode(signed)))\n const expected = btoa(String.fromCharCode(...digest))\n .replaceAll('+', '-')\n .replaceAll('/', '_')\n .replaceAll('=', '')\n return expected === signature\n}\n\nexport type RateLimitStoreProbe = {\n increment: (\n key: string,\n windowSeconds: number,\n now: number,\n ) => Promise<{ count: number; resetAt: number }> | { count: number; resetAt: number }\n}\n\n// The package cannot reach a consumer's PostgreSQL, so the store contract travels to wherever the real database is.\nexport async function assertRateLimitStoreConformance(store: RateLimitStoreProbe, options: { now?: number } = {}) {\n const now = options.now ?? Math.floor(Date.now() / 1000)\n const window = 60\n const key = `conformance:${crypto.randomUUID()}`\n\n const first = await store.increment(key, window, now)\n if (first.count !== 1) throw new ConformanceError('rate limit store', `expected a new key to start at 1, received ${first.count}`)\n if (first.resetAt !== now + window) {\n throw new ConformanceError('rate limit store', `expected the window to end at ${now + window}, received ${first.resetAt}`)\n }\n\n const second = await store.increment(key, window, now + 1)\n if (second.count !== 2) throw new ConformanceError('rate limit store', `expected the second request to count 2, received ${second.count}`)\n if (second.resetAt !== first.resetAt) {\n throw new ConformanceError('rate limit store', 'a request inside the window must not extend it')\n }\n\n const other = await store.increment(`conformance:${crypto.randomUUID()}`, window, now)\n if (other.count !== 1) throw new ConformanceError('rate limit store', 'one key consumed another key budget')\n\n const expired = await store.increment(key, window, first.resetAt)\n if (expired.count !== 1) throw new ConformanceError('rate limit store', 'an elapsed window must start a new count')\n if (expired.resetAt !== first.resetAt + window) {\n throw new ConformanceError('rate limit store', 'an elapsed window must start a new window')\n }\n}\n\nexport type AuthSecretProvider = (environment: NodeJS.ProcessEnv) => string | Promise<string>\n\n// A secret that changes between restarts signs out every session, and a short one is guessable. Neither shows up\n// until it is already in production, so the contract is checked against the provider an application wired.\nexport async function assertAuthSecretConformance(secret: AuthSecretProvider, options: { environmentKey?: string } = {}) {\n const key = options.environmentKey ?? 'AUTH_SECRET'\n const configured = 'configured-secret-value-that-is-long-enough'\n\n const first = await secret({})\n if (typeof first !== 'string' || !first.trim()) throw new ConformanceError('auth secret', 'provider returned no secret')\n if (first.length < 32) throw new ConformanceError('auth secret', `expected at least 32 characters, received ${first.length}`)\n if (new Set(first).size < 8) throw new ConformanceError('auth secret', 'secret does not look randomly generated')\n\n if ((await secret({})) !== first) {\n throw new ConformanceError('auth secret', 'secret changed between calls, which signs out every session on restart')\n }\n if ((await secret({ [key]: configured })) !== configured) {\n throw new ConformanceError('auth secret', `${key} must take precedence over a generated secret`)\n }\n}\n\nexport type RealtimePublisherProbe = { publish: (channel: string, data: unknown) => boolean; close: () => Promise<void> }\n\n// A publisher that accepts every channel turns a burst into unbounded memory, and one that accepts work after close\n// loses publications during shutdown. Both appear only under load.\nexport async function assertRealtimePublisherConformance(create: () => RealtimePublisherProbe, options: { probes?: number } = {}) {\n const probes = options.probes ?? 5_000\n const bounded = create()\n let admitted = 0\n for (let attempt = 0; attempt < probes; attempt++) if (bounded.publish(`conformance-${attempt}`, { attempt })) admitted += 1\n if (admitted === probes) throw new ConformanceError('realtime publisher', `accepted all ${probes} channels without a capacity limit`)\n if (admitted === 0) throw new ConformanceError('realtime publisher', 'rejected every publication')\n await bounded.close()\n\n const closed = create()\n await closed.close()\n if (closed.publish('conformance', {})) throw new ConformanceError('realtime publisher', 'accepted work after it was closed')\n}\n\nexport type SmtpConfigReader = (environment: NodeJS.ProcessEnv) => { host: string; port: number; from: string } | undefined\n\n// Half-configured SMTP is the failure that reaches production, because nothing sends mail until something needs to.\nexport function assertSmtpConfigConformance(read: SmtpConfigReader) {\n if (read({}) !== undefined) throw new ConformanceError('SMTP configuration', 'expected no configuration when nothing is set')\n\n const configured = read({ SMTP_HOST: 'smtp.example', EMAIL_FROM: 'app@example.com' })\n if (!configured) throw new ConformanceError('SMTP configuration', 'expected a configuration from a host and sender')\n if (configured.port !== 587)\n throw new ConformanceError('SMTP configuration', `expected the default port 587, received ${configured.port}`)\n\n rejects(read, { SMTP_HOST: 'smtp.example' }, 'a host without a sender')\n rejects(read, { EMAIL_FROM: 'app@example.com' }, 'a sender without a host')\n rejects(read, { SMTP_HOST: 'smtp.example', EMAIL_FROM: 'app@example.com', SMTP_PORT: '70000' }, 'a port outside the valid range')\n rejects(read, { SMTP_HOST: 'smtp.example', EMAIL_FROM: 'app@example.com', SMTP_USER: 'user' }, 'a user without a password')\n}\n\nfunction rejects(read: SmtpConfigReader, environment: NodeJS.ProcessEnv, scenario: string) {\n try {\n read(environment)\n } catch {\n return\n }\n throw new ConformanceError('SMTP configuration', `expected ${scenario} to be rejected`)\n}\n\nexport type DatabaseTarget = { provider: 'sqlite'; file: string } | { provider: 'postgres'; url: string }\n\nexport type DatabaseTargetResolver = (options: { databaseUrl?: string; sqliteFile: string }) => DatabaseTarget\n\nexport function assertDatabaseTargetConformance(resolve: DatabaseTargetResolver) {\n const sqliteFile = '/data/application.sqlite'\n const sqlite = resolve({ sqliteFile })\n if (sqlite.provider !== 'sqlite' || sqlite.file !== sqliteFile) {\n throw new ConformanceError('default database target', 'expected the configured SQLite file')\n }\n\n for (const url of ['postgres://user:secret@database/application', 'postgresql://user:secret@database/application']) {\n const target = resolve({ databaseUrl: url, sqliteFile })\n if (target.provider !== 'postgres' || target.url !== url) {\n throw new ConformanceError('PostgreSQL database target', `expected the configured ${new URL(url).protocol} URL`)\n }\n }\n\n try {\n resolve({ databaseUrl: 'https://database.example/application', sqliteFile })\n } catch {\n return\n }\n throw new ConformanceError('invalid database target', 'expected a non-PostgreSQL URL to be rejected')\n}\n\nexport function assertPostHogBrowserConformance(options: Record<string, unknown>) {\n if (typeof options.api_host !== 'string' || !options.api_host.trim()) {\n throw new ConformanceError('PostHog browser initialization', 'api_host must be configured')\n }\n if (typeof options.ui_host !== 'string' || !options.ui_host.trim()) {\n throw new ConformanceError('PostHog browser initialization', 'ui_host must be configured')\n }\n if (typeof options.defaults !== 'string' || !options.defaults.trim()) {\n throw new ConformanceError('PostHog browser initialization', 'SDK defaults must be pinned')\n }\n if (!options.capture_exceptions) {\n throw new ConformanceError('PostHog browser initialization', 'exception autocapture must be enabled')\n }\n if (options.capture_pageview !== 'history_change') {\n throw new ConformanceError('PostHog browser initialization', 'SPA pageviews must follow history changes')\n }\n if (options.person_profiles !== 'identified_only') {\n throw new ConformanceError('PostHog browser initialization', 'person profiles must be limited to identified users')\n }\n const recording = options.session_recording\n if (!recording || typeof recording !== 'object' || !('maskAllInputs' in recording) || recording.maskAllInputs !== true) {\n throw new ConformanceError('PostHog browser initialization', 'session replay must mask all inputs by default')\n }\n}\n\ntype PostHogContextParser = (\n request: Request,\n options?: { authenticatedDistinctId?: string; allowAnonymousDistinctId?: boolean },\n) => { distinctId?: string; sessionId?: string; properties: { $session_id?: string } }\n\nexport function assertPostHogRequestConformance(parse: PostHogContextParser) {\n const matched = parse(postHogRequest('person-123', 'session-456'), { authenticatedDistinctId: 'person-123' })\n if (matched.distinctId !== 'person-123' || matched.sessionId !== 'session-456' || matched.properties.$session_id !== 'session-456') {\n throw new ConformanceError('authenticated PostHog request', 'expected verified identity and session propagation')\n }\n const spoofed = parse(postHogRequest('attacker', 'session-456'), { authenticatedDistinctId: 'person-123' })\n if (spoofed.distinctId !== undefined) {\n throw new ConformanceError('spoofed PostHog request', 'unverified distinct id was trusted')\n }\n const malformed = parse(postHogRequest('person-123', 'x'.repeat(129)), { authenticatedDistinctId: 'person-123' })\n if (malformed.sessionId !== undefined || malformed.properties.$session_id !== undefined) {\n throw new ConformanceError('malformed PostHog request', 'unbounded session id was propagated')\n }\n}\n\nfunction postHogRequest(distinctId: string, sessionId: string) {\n return new Request('https://app.example/action', {\n headers: { 'x-posthog-distinct-id': distinctId, 'x-posthog-session-id': sessionId },\n })\n}\n\nasync function accepted(scenario: string, work: () => void | Promise<void>) {\n try {\n await work()\n } catch (error) {\n throw new ConformanceError(scenario, 'expected request to be accepted', { cause: error })\n }\n}\n\nasync function rejected(scenario: string, work: () => void | Promise<void>) {\n try {\n await work()\n } catch {\n return\n }\n throw new ConformanceError(scenario, 'expected request to be rejected')\n}\n\nasync function responseBody(response: Response, scenario: string): Promise<Record<string, unknown>> {\n try {\n const body: unknown = await response.json()\n if (body && typeof body === 'object') return body as Record<string, unknown>\n } catch (error) {\n throw new ConformanceError(scenario, 'response body must be JSON', { cause: error })\n }\n throw new ConformanceError(scenario, 'response body must be an object')\n}\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/conformance/index.ts"],"names":[],"mappings":"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","sourcesContent":["export class ConformanceError extends Error {\n constructor(\n readonly scenario: string,\n message: string,\n options: { cause?: unknown } = {},\n ) {\n super(`${scenario}: ${message}`, options.cause === undefined ? undefined : { cause: options.cause })\n this.name = 'ConformanceError'\n }\n}\n\nexport async function assertMutationOriginConformance(\n guard: (request: Request) => void | Promise<void>,\n options: { trustForwardedHeaders?: boolean } = {},\n) {\n await accepted('same-origin request', () =>\n guard(\n new Request('https://app.example/action', {\n method: 'POST',\n headers: { origin: 'https://app.example', 'sec-fetch-site': 'same-origin' },\n }),\n ),\n )\n await rejected('cross-origin request', () =>\n guard(\n new Request('https://app.example/action', {\n method: 'POST',\n headers: { origin: 'https://attacker.example', 'sec-fetch-site': 'cross-site' },\n }),\n ),\n )\n await rejected('missing-origin request', () => guard(new Request('https://app.example/action', { method: 'POST' })))\n if (!options.trustForwardedHeaders) {\n await rejected('spoofed forwarded-origin request', () =>\n guard(\n new Request('https://app.example/action', {\n method: 'POST',\n headers: {\n origin: 'https://attacker.example',\n 'x-forwarded-host': 'attacker.example',\n 'x-forwarded-proto': 'https',\n },\n }),\n ),\n )\n }\n}\n\nexport type HealthHandlerFactory = (check: () => void | Promise<void>) => () => Response | Promise<Response>\n\nexport async function assertHealthHandlerConformance(createHandler: HealthHandlerFactory) {\n const healthy = await createHandler(() => undefined)()\n if (healthy.status !== 200) throw new ConformanceError('healthy dependency', `expected status 200, received ${healthy.status}`)\n const healthyBody = await responseBody(healthy, 'healthy dependency')\n if (healthyBody.ok !== true) throw new ConformanceError('healthy dependency', 'response body must contain ok: true')\n\n const privateMessage = 'password=private-diagnostic'\n const unavailable = await createHandler(() => Promise.reject(new Error(privateMessage)))()\n if (unavailable.status !== 503) {\n throw new ConformanceError('unavailable dependency', `expected status 503, received ${unavailable.status}`)\n }\n const unavailableText = await unavailable.text()\n if (unavailableText.includes(privateMessage)) {\n throw new ConformanceError('unavailable dependency', 'response exposed the private diagnostic message')\n }\n let unavailableBody: unknown\n try {\n unavailableBody = JSON.parse(unavailableText)\n } catch (error) {\n throw new ConformanceError('unavailable dependency', 'response body must be JSON', { cause: error })\n }\n if (!unavailableBody || typeof unavailableBody !== 'object' || !('ok' in unavailableBody) || unavailableBody.ok !== false) {\n throw new ConformanceError('unavailable dependency', 'response body must contain ok: false')\n }\n}\n\nexport type SqlitePragmaReader = (name: 'journal_mode' | 'synchronous' | 'busy_timeout' | 'foreign_keys') => unknown\n\nexport async function assertSqliteConformance(readPragma: SqlitePragmaReader) {\n const values = {\n journalMode: String(await readPragma('journal_mode')).toLowerCase(),\n synchronous: Number(await readPragma('synchronous')),\n busyTimeout: Number(await readPragma('busy_timeout')),\n foreignKeys: Number(await readPragma('foreign_keys')),\n }\n if (values.journalMode !== 'wal' && values.journalMode !== 'memory') {\n throw new ConformanceError('SQLite journal mode', `expected wal or memory, received ${values.journalMode}`)\n }\n if (values.synchronous !== 2) {\n throw new ConformanceError('SQLite synchronous mode', `expected FULL (2), received ${values.synchronous}`)\n }\n if (values.busyTimeout !== 5000) {\n throw new ConformanceError('SQLite busy timeout', `expected 5000, received ${values.busyTimeout}`)\n }\n if (values.foreignKeys !== 1) {\n throw new ConformanceError('SQLite foreign keys', `expected enabled (1), received ${values.foreignKeys}`)\n }\n}\n\nexport type RealtimeTokenSigner = (subject: string, claims: Record<string, unknown>) => string | Promise<string>\n\nexport type RealtimeTokenConformanceOptions = { secret: string; maxTtlSeconds?: number; now?: number }\n\nexport async function assertRealtimeTokenConformance(sign: RealtimeTokenSigner, options: RealtimeTokenConformanceOptions) {\n const now = options.now ?? Math.floor(Date.now() / 1000)\n const maxTtlSeconds = options.maxTtlSeconds ?? 60 * 60\n const token = await sign('person-123', { channel: 'room:1' })\n const { header, payload, signed, signature } = decodeToken(token)\n\n if (header.alg !== 'HS256') {\n throw new ConformanceError('realtime token algorithm', `expected HS256, received ${JSON.stringify(header.alg)}`)\n }\n if (payload.sub !== 'person-123') {\n throw new ConformanceError('realtime token subject', 'token must bind the subject it was signed for')\n }\n if (payload.channel !== 'room:1') {\n throw new ConformanceError('realtime token claims', 'token must carry the claims it was signed with')\n }\n if (typeof payload.exp !== 'number') {\n throw new ConformanceError('realtime token expiry', 'token must expire')\n }\n if (payload.exp <= now) {\n throw new ConformanceError('realtime token expiry', 'token expired before it was issued')\n }\n if (payload.exp - now > maxTtlSeconds) {\n throw new ConformanceError('realtime token expiry', `token outlives the ${maxTtlSeconds} second maximum`)\n }\n if (!(await verifyHmac(signed, signature, options.secret))) {\n throw new ConformanceError('realtime token signature', 'token is not signed with the shared Centrifugo secret')\n }\n\n const other = decodeToken(await sign('person-456', { channel: 'room:1' })).payload\n if (other.sub === payload.sub) {\n throw new ConformanceError('realtime token subject', 'every subject received the same identity')\n }\n}\n\nfunction decodeToken(token: string) {\n const segments = token.split('.')\n if (segments.length !== 3) throw new ConformanceError('realtime token format', 'expected a three-segment JWT')\n const [header, claims, signature] = segments as [string, string, string]\n return {\n header: decodeSegment(header, 'header'),\n payload: decodeSegment(claims, 'payload'),\n signed: `${header}.${claims}`,\n signature,\n }\n}\n\nfunction decodeSegment(segment: string, name: string): Record<string, unknown> {\n try {\n const padded = segment\n .replaceAll('-', '+')\n .replaceAll('_', '/')\n .padEnd(Math.ceil(segment.length / 4) * 4, '=')\n const value: unknown = JSON.parse(atob(padded))\n if (value && typeof value === 'object') return value as Record<string, unknown>\n } catch (error) {\n throw new ConformanceError('realtime token format', `${name} is not base64url JSON`, { cause: error })\n }\n throw new ConformanceError('realtime token format', `${name} is not an object`)\n}\n\nasync function verifyHmac(signed: string, signature: string, secret: string) {\n const encoder = new TextEncoder()\n const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])\n const digest = new Uint8Array(await crypto.subtle.sign('HMAC', key, encoder.encode(signed)))\n const expected = btoa(String.fromCharCode(...digest))\n .replaceAll('+', '-')\n .replaceAll('/', '_')\n .replaceAll('=', '')\n return expected === signature\n}\n\nexport type RateLimitStoreProbe = {\n increment: (\n key: string,\n windowSeconds: number,\n now: number,\n ) => Promise<{ count: number; resetAt: number }> | { count: number; resetAt: number }\n}\n\n// The package cannot reach a consumer's PostgreSQL, so the store contract travels to wherever the real database is.\nexport async function assertRateLimitStoreConformance(store: RateLimitStoreProbe, options: { now?: number } = {}) {\n const now = options.now ?? Math.floor(Date.now() / 1000)\n const window = 60\n const key = `conformance:${crypto.randomUUID()}`\n\n const first = await store.increment(key, window, now)\n if (first.count !== 1) throw new ConformanceError('rate limit store', `expected a new key to start at 1, received ${first.count}`)\n if (first.resetAt !== now + window) {\n throw new ConformanceError('rate limit store', `expected the window to end at ${now + window}, received ${first.resetAt}`)\n }\n\n const second = await store.increment(key, window, now + 1)\n if (second.count !== 2) throw new ConformanceError('rate limit store', `expected the second request to count 2, received ${second.count}`)\n if (second.resetAt !== first.resetAt) {\n throw new ConformanceError('rate limit store', 'a request inside the window must not extend it')\n }\n\n const other = await store.increment(`conformance:${crypto.randomUUID()}`, window, now)\n if (other.count !== 1) throw new ConformanceError('rate limit store', 'one key consumed another key budget')\n\n const expired = await store.increment(key, window, first.resetAt)\n if (expired.count !== 1) throw new ConformanceError('rate limit store', 'an elapsed window must start a new count')\n if (expired.resetAt !== first.resetAt + window) {\n throw new ConformanceError('rate limit store', 'an elapsed window must start a new window')\n }\n}\n\nexport type AuthSecretProvider = (environment: NodeJS.ProcessEnv) => string | Promise<string>\n\n// A secret that changes between restarts signs out every session, and a short one is guessable. Neither shows up\n// until it is already in production, so the contract is checked against the provider an application wired.\nexport async function assertAuthSecretConformance(secret: AuthSecretProvider, options: { environmentKey?: string } = {}) {\n const key = options.environmentKey ?? 'AUTH_SECRET'\n const configured = 'configured-secret-value-that-is-long-enough'\n\n const first = await secret({})\n if (typeof first !== 'string' || !first.trim()) throw new ConformanceError('auth secret', 'provider returned no secret')\n if (first.length < 32) throw new ConformanceError('auth secret', `expected at least 32 characters, received ${first.length}`)\n if (new Set(first).size < 8) throw new ConformanceError('auth secret', 'secret does not look randomly generated')\n\n if ((await secret({})) !== first) {\n throw new ConformanceError('auth secret', 'secret changed between calls, which signs out every session on restart')\n }\n if ((await secret({ [key]: configured })) !== configured) {\n throw new ConformanceError('auth secret', `${key} must take precedence over a generated secret`)\n }\n}\n\nexport type RealtimePublisherProbe = { publish: (channel: string, data: unknown) => boolean; close: () => Promise<void> }\n\n// A publisher that accepts every channel turns a burst into unbounded memory, and one that accepts work after close\n// loses publications during shutdown. Both appear only under load.\nexport async function assertRealtimePublisherConformance(create: () => RealtimePublisherProbe, options: { probes?: number } = {}) {\n const probes = options.probes ?? 5_000\n const bounded = create()\n let admitted = 0\n for (let attempt = 0; attempt < probes; attempt++) if (bounded.publish(`conformance-${attempt}`, { attempt })) admitted += 1\n if (admitted === probes) throw new ConformanceError('realtime publisher', `accepted all ${probes} channels without a capacity limit`)\n if (admitted === 0) throw new ConformanceError('realtime publisher', 'rejected every publication')\n await bounded.close()\n\n const closed = create()\n await closed.close()\n if (closed.publish('conformance', {})) throw new ConformanceError('realtime publisher', 'accepted work after it was closed')\n}\n\nexport type SmtpConfigReader = (environment: NodeJS.ProcessEnv) => { host: string; port: number; from: string } | undefined\n\n// Half-configured SMTP is the failure that reaches production, because nothing sends mail until something needs to.\nexport function assertSmtpConfigConformance(read: SmtpConfigReader) {\n if (read({}) !== undefined) throw new ConformanceError('SMTP configuration', 'expected no configuration when nothing is set')\n\n const configured = read({ SMTP_HOST: 'smtp.example', EMAIL_FROM: 'app@example.com' })\n if (!configured) throw new ConformanceError('SMTP configuration', 'expected a configuration from a host and sender')\n if (configured.port !== 587)\n throw new ConformanceError('SMTP configuration', `expected the default port 587, received ${configured.port}`)\n\n rejects(read, { SMTP_HOST: 'smtp.example' }, 'a host without a sender')\n rejects(read, { EMAIL_FROM: 'app@example.com' }, 'a sender without a host')\n rejects(read, { SMTP_HOST: 'smtp.example', EMAIL_FROM: 'app@example.com', SMTP_PORT: '70000' }, 'a port outside the valid range')\n rejects(read, { SMTP_HOST: 'smtp.example', EMAIL_FROM: 'app@example.com', SMTP_USER: 'user' }, 'a user without a password')\n}\n\nfunction rejects(read: SmtpConfigReader, environment: NodeJS.ProcessEnv, scenario: string) {\n try {\n read(environment)\n } catch {\n return\n }\n throw new ConformanceError('SMTP configuration', `expected ${scenario} to be rejected`)\n}\n\nexport type DatabaseTarget = { provider: 'sqlite'; file: string } | { provider: 'postgres'; url: string }\n\nexport type DatabaseTargetResolver = (options: { databaseUrl?: string; sqliteFile: string }) => DatabaseTarget\n\nexport function assertDatabaseTargetConformance(resolve: DatabaseTargetResolver) {\n const sqliteFile = '/data/application.sqlite'\n const sqlite = resolve({ sqliteFile })\n if (sqlite.provider !== 'sqlite' || sqlite.file !== sqliteFile) {\n throw new ConformanceError('default database target', 'expected the configured SQLite file')\n }\n\n for (const url of ['postgres://user:secret@database/application', 'postgresql://user:secret@database/application']) {\n const target = resolve({ databaseUrl: url, sqliteFile })\n if (target.provider !== 'postgres' || target.url !== url) {\n throw new ConformanceError('PostgreSQL database target', `expected the configured ${new URL(url).protocol} URL`)\n }\n }\n\n try {\n resolve({ databaseUrl: 'https://database.example/application', sqliteFile })\n } catch {\n return\n }\n throw new ConformanceError('invalid database target', 'expected a non-PostgreSQL URL to be rejected')\n}\n\nexport function assertPostHogBrowserConformance(options: Record<string, unknown>) {\n if (typeof options.api_host !== 'string' || !options.api_host.trim()) {\n throw new ConformanceError('PostHog browser initialization', 'api_host must be configured')\n }\n if (typeof options.ui_host !== 'string' || !options.ui_host.trim()) {\n throw new ConformanceError('PostHog browser initialization', 'ui_host must be configured')\n }\n if (typeof options.defaults !== 'string' || !options.defaults.trim()) {\n throw new ConformanceError('PostHog browser initialization', 'SDK defaults must be pinned')\n }\n if (!options.capture_exceptions) {\n throw new ConformanceError('PostHog browser initialization', 'exception autocapture must be enabled')\n }\n if (options.capture_pageview !== 'history_change') {\n throw new ConformanceError('PostHog browser initialization', 'SPA pageviews must follow history changes')\n }\n if (options.person_profiles !== 'identified_only') {\n throw new ConformanceError('PostHog browser initialization', 'person profiles must be limited to identified users')\n }\n if (options.mask_personal_data_properties !== true) {\n throw new ConformanceError('PostHog browser initialization', 'personal-data URL properties must be masked')\n }\n if (!Array.isArray(options.custom_personal_data_properties) || !options.custom_personal_data_properties.includes('token')) {\n throw new ConformanceError('PostHog browser initialization', 'token query parameters must be masked')\n }\n const recording = options.session_recording\n if (!recording || typeof recording !== 'object' || !('maskAllInputs' in recording) || recording.maskAllInputs !== true) {\n throw new ConformanceError('PostHog browser initialization', 'session replay must mask all inputs by default')\n }\n}\n\ntype PostHogContextParser = (\n request: Request,\n options?: { authenticatedDistinctId?: string; allowAnonymousDistinctId?: boolean },\n) => { distinctId?: string; sessionId?: string; properties: { $session_id?: string } }\n\nexport function assertPostHogRequestConformance(parse: PostHogContextParser) {\n const matched = parse(postHogRequest('person-123', 'session-456'), { authenticatedDistinctId: 'person-123' })\n if (matched.distinctId !== 'person-123' || matched.sessionId !== 'session-456' || matched.properties.$session_id !== 'session-456') {\n throw new ConformanceError('authenticated PostHog request', 'expected verified identity and session propagation')\n }\n const spoofed = parse(postHogRequest('attacker', 'session-456'), { authenticatedDistinctId: 'person-123' })\n if (spoofed.distinctId !== undefined) {\n throw new ConformanceError('spoofed PostHog request', 'unverified distinct id was trusted')\n }\n const malformed = parse(postHogRequest('person-123', 'x'.repeat(129)), { authenticatedDistinctId: 'person-123' })\n if (malformed.sessionId !== undefined || malformed.properties.$session_id !== undefined) {\n throw new ConformanceError('malformed PostHog request', 'unbounded session id was propagated')\n }\n}\n\nfunction postHogRequest(distinctId: string, sessionId: string) {\n return new Request('https://app.example/action', {\n headers: { 'x-posthog-distinct-id': distinctId, 'x-posthog-session-id': sessionId },\n })\n}\n\nasync function accepted(scenario: string, work: () => void | Promise<void>) {\n try {\n await work()\n } catch (error) {\n throw new ConformanceError(scenario, 'expected request to be accepted', { cause: error })\n }\n}\n\nasync function rejected(scenario: string, work: () => void | Promise<void>) {\n try {\n await work()\n } catch {\n return\n }\n throw new ConformanceError(scenario, 'expected request to be rejected')\n}\n\nasync function responseBody(response: Response, scenario: string): Promise<Record<string, unknown>> {\n try {\n const body: unknown = await response.json()\n if (body && typeof body === 'object') return body as Record<string, unknown>\n } catch (error) {\n throw new ConformanceError(scenario, 'response body must be JSON', { cause: error })\n }\n throw new ConformanceError(scenario, 'response body must be an object')\n}\n"]}
|
package/dist/email/index.d.ts
CHANGED
|
@@ -25,6 +25,25 @@ export type EmailDelivery = {
|
|
|
25
25
|
send(message: EmailMessage): Promise<void>;
|
|
26
26
|
verify(): Promise<void>;
|
|
27
27
|
};
|
|
28
|
+
export type AuthEmailInput<User extends {
|
|
29
|
+
email: string;
|
|
30
|
+
} = {
|
|
31
|
+
email: string;
|
|
32
|
+
}> = {
|
|
33
|
+
user: User;
|
|
34
|
+
url: string;
|
|
35
|
+
token: string;
|
|
36
|
+
};
|
|
37
|
+
export type AuthEmailMessageFactory<User extends {
|
|
38
|
+
email: string;
|
|
39
|
+
} = {
|
|
40
|
+
email: string;
|
|
41
|
+
}> = (input: AuthEmailInput<User>, request?: Request) => EmailMessage | Promise<EmailMessage>;
|
|
42
|
+
export declare function createAuthEmailHandler<User extends {
|
|
43
|
+
email: string;
|
|
44
|
+
} = {
|
|
45
|
+
email: string;
|
|
46
|
+
}>(delivery: EmailDelivery, message: AuthEmailMessageFactory<User>): (input: AuthEmailInput<User>, request?: Request) => Promise<void>;
|
|
28
47
|
export declare function smtpConfigFromEnvironment(environment?: NodeJS.ProcessEnv, keys?: SmtpEnvironmentOptions): SmtpConfig | undefined;
|
|
29
48
|
export declare function createSmtpDelivery(config: SmtpConfig): EmailDelivery;
|
|
30
49
|
export declare function createSmtpTransport(config: SmtpConfig): nodemailer.Transporter<import("nodemailer/lib/smtp-transport/index.js").SentMessageInfo, import("nodemailer/lib/smtp-transport/index.js").Options>;
|
package/dist/email/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import nodemailer from 'nodemailer';
|
|
2
|
+
export function createAuthEmailHandler(delivery, message) {
|
|
3
|
+
return async (input, request) => delivery.send(await message(input, request));
|
|
4
|
+
}
|
|
2
5
|
export function smtpConfigFromEnvironment(environment = process.env, keys = {}) {
|
|
3
6
|
const host = environment[keys.host ?? 'SMTP_HOST']?.trim();
|
|
4
7
|
const from = environment[keys.from ?? 'EMAIL_FROM']?.trim();
|
package/dist/email/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/email/index.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/email/index.ts"],"names":[],"mappings":"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","sourcesContent":["import nodemailer from 'nodemailer'\n\nexport type EmailMessage = { to: string; subject: string; text: string; html?: string }\n\nexport type SmtpConfig = {\n from: string\n host: string\n port: number\n secure: boolean\n user?: string\n password?: string\n}\n\nexport type SmtpEnvironmentOptions = {\n host?: string\n port?: string\n secure?: string\n user?: string\n password?: string\n from?: string\n}\n\nexport type EmailDelivery = {\n send(message: EmailMessage): Promise<void>\n verify(): Promise<void>\n}\n\nexport type AuthEmailInput<User extends { email: string } = { email: string }> = {\n user: User\n url: string\n token: string\n}\n\nexport type AuthEmailMessageFactory<User extends { email: string } = { email: string }> = (\n input: AuthEmailInput<User>,\n request?: Request,\n) => EmailMessage | Promise<EmailMessage>\n\nexport function createAuthEmailHandler<User extends { email: string } = { email: string }>(\n delivery: EmailDelivery,\n message: AuthEmailMessageFactory<User>,\n) {\n return async (input: AuthEmailInput<User>, request?: Request) => delivery.send(await message(input, request))\n}\n\nexport function smtpConfigFromEnvironment(\n environment: NodeJS.ProcessEnv = process.env,\n keys: SmtpEnvironmentOptions = {},\n): SmtpConfig | undefined {\n const host = environment[keys.host ?? 'SMTP_HOST']?.trim()\n const from = environment[keys.from ?? 'EMAIL_FROM']?.trim()\n if (!host && !from) return undefined\n if (!host) throw new Error(`${keys.host ?? 'SMTP_HOST'} is required for SMTP email`)\n if (!from) throw new Error(`${keys.from ?? 'EMAIL_FROM'} is required for SMTP email`)\n\n const portKey = keys.port ?? 'SMTP_PORT'\n const port = Number(environment[portKey] ?? 587)\n if (!Number.isInteger(port) || port < 1 || port > 65_535) throw new Error(`${portKey} must be a valid TCP port`)\n\n const userKey = keys.user ?? 'SMTP_USER'\n const passwordKey = keys.password ?? 'SMTP_PASSWORD'\n const user = environment[userKey]?.trim()\n const password = environment[passwordKey]\n if (Boolean(user) !== Boolean(password)) throw new Error(`${userKey} and ${passwordKey} must be configured together`)\n\n return {\n from,\n host,\n port,\n secure: environment[keys.secure ?? 'SMTP_SECURE']?.trim().toLowerCase() === 'true',\n ...(user && password ? { user, password } : {}),\n }\n}\n\nexport function createSmtpDelivery(config: SmtpConfig): EmailDelivery {\n const transport = createSmtpTransport(config)\n return {\n send: async (message) => transport.sendMail({ from: config.from, ...message }).then(() => undefined),\n verify: async () => transport.verify().then(() => undefined),\n }\n}\n\nexport function createSmtpTransport(config: SmtpConfig) {\n return nodemailer.createTransport({\n host: config.host,\n port: config.port,\n secure: config.secure,\n auth: config.user ? { user: config.user, pass: config.password } : undefined,\n })\n}\n"]}
|
package/dist/posthog/client.js
CHANGED
|
@@ -8,6 +8,8 @@ export function postHogBrowserOptions(input) {
|
|
|
8
8
|
defaults: POSTHOG_BROWSER_DEFAULTS,
|
|
9
9
|
capture_exceptions: true,
|
|
10
10
|
capture_pageview: 'history_change',
|
|
11
|
+
custom_personal_data_properties: ['token'],
|
|
12
|
+
mask_personal_data_properties: true,
|
|
11
13
|
person_profiles: 'identified_only',
|
|
12
14
|
session_recording: { maskAllInputs: true, blockSelector: '.ph-no-capture' },
|
|
13
15
|
...(input.tracingHostnames ? { tracing_headers: input.tracingHostnames } : {}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/posthog/client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAEvG,OAAO,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAA;AACpF,MAAM,CAAC,MAAM,wBAAwB,GAAG,YAAY,CAAA;AAEpD,MAAM,UAAU,qBAAqB,CAAC,KAKrC;IACC,OAAO;QACL,QAAQ,EAAE,QAAQ,CAAC,KAAK,CAAC,OAAO,EAAE,SAAS,CAAC;QAC5C,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,QAAQ,CAAC;QACzC,QAAQ,EAAE,wBAAwB;QAClC,kBAAkB,EAAE,IAAI;QACxB,gBAAgB,EAAE,gBAAgB;QAClC,eAAe,EAAE,iBAAiB;QAClC,iBAAiB,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,aAAa,EAAE,gBAAgB,EAAE;QAC3E,GAAG,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,KAAK,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9E,GAAG,KAAK,CAAC,OAAO;KACjB,CAAA;AACH,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,MAA2E;IAC/G,MAAM,UAAU,GAAG,iBAAiB,CAAC,MAAM,CAAC,eAAe,EAAE,CAAC,CAAA;IAC9D,MAAM,SAAS,GAAG,iBAAiB,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC,CAAA;IAC5D,OAAO;QACL,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,0BAA0B,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnE,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,yBAAyB,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACjE,CAAA;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,KAAa,EAAE,IAAY;IAC3C,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,CAAA;IAC/B,IAAI,CAAC,UAAU;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,cAAc,CAAC,CAAA;IACvD,OAAO,UAAU,CAAA;AACnB,CAAC","sourcesContent":["import type { PostHogConfig } from 'posthog-js'\nimport { POSTHOG_DISTINCT_ID_HEADER, POSTHOG_SESSION_ID_HEADER, postHogIdentifier } from './request.js'\n\nexport { POSTHOG_DISTINCT_ID_HEADER, POSTHOG_SESSION_ID_HEADER } from './request.js'\nexport const POSTHOG_BROWSER_DEFAULTS = '2026-05-30'\n\nexport function postHogBrowserOptions(input: {\n apiHost: string\n uiHost: string\n tracingHostnames?: string[]\n options?: Partial<PostHogConfig>\n}): Partial<PostHogConfig> {\n return {\n api_host: required(input.apiHost, 'apiHost'),\n ui_host: required(input.uiHost, 'uiHost'),\n defaults: POSTHOG_BROWSER_DEFAULTS,\n capture_exceptions: true,\n capture_pageview: 'history_change',\n person_profiles: 'identified_only',\n session_recording: { maskAllInputs: true, blockSelector: '.ph-no-capture' },\n ...(input.tracingHostnames ? { tracing_headers: input.tracingHostnames } : {}),\n ...input.options,\n }\n}\n\nexport function postHogBrowserHeaders(client: { get_distinct_id(): string; get_session_id(): string | undefined }): Record<string, string> {\n const distinctId = postHogIdentifier(client.get_distinct_id())\n const sessionId = postHogIdentifier(client.get_session_id())\n return {\n ...(distinctId ? { [POSTHOG_DISTINCT_ID_HEADER]: distinctId } : {}),\n ...(sessionId ? { [POSTHOG_SESSION_ID_HEADER]: sessionId } : {}),\n }\n}\n\nfunction required(value: string, name: string) {\n const normalized = value.trim()\n if (!normalized) throw new Error(`${name} is required`)\n return normalized\n}\n"]}
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/posthog/client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAEvG,OAAO,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAA;AACpF,MAAM,CAAC,MAAM,wBAAwB,GAAG,YAAY,CAAA;AAEpD,MAAM,UAAU,qBAAqB,CAAC,KAKrC;IACC,OAAO;QACL,QAAQ,EAAE,QAAQ,CAAC,KAAK,CAAC,OAAO,EAAE,SAAS,CAAC;QAC5C,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,QAAQ,CAAC;QACzC,QAAQ,EAAE,wBAAwB;QAClC,kBAAkB,EAAE,IAAI;QACxB,gBAAgB,EAAE,gBAAgB;QAClC,+BAA+B,EAAE,CAAC,OAAO,CAAC;QAC1C,6BAA6B,EAAE,IAAI;QACnC,eAAe,EAAE,iBAAiB;QAClC,iBAAiB,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,aAAa,EAAE,gBAAgB,EAAE;QAC3E,GAAG,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,KAAK,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9E,GAAG,KAAK,CAAC,OAAO;KACjB,CAAA;AACH,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,MAA2E;IAC/G,MAAM,UAAU,GAAG,iBAAiB,CAAC,MAAM,CAAC,eAAe,EAAE,CAAC,CAAA;IAC9D,MAAM,SAAS,GAAG,iBAAiB,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC,CAAA;IAC5D,OAAO;QACL,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,0BAA0B,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnE,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,yBAAyB,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACjE,CAAA;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,KAAa,EAAE,IAAY;IAC3C,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,CAAA;IAC/B,IAAI,CAAC,UAAU;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,cAAc,CAAC,CAAA;IACvD,OAAO,UAAU,CAAA;AACnB,CAAC","sourcesContent":["import type { PostHogConfig } from 'posthog-js'\nimport { POSTHOG_DISTINCT_ID_HEADER, POSTHOG_SESSION_ID_HEADER, postHogIdentifier } from './request.js'\n\nexport { POSTHOG_DISTINCT_ID_HEADER, POSTHOG_SESSION_ID_HEADER } from './request.js'\nexport const POSTHOG_BROWSER_DEFAULTS = '2026-05-30'\n\nexport function postHogBrowserOptions(input: {\n apiHost: string\n uiHost: string\n tracingHostnames?: string[]\n options?: Partial<PostHogConfig>\n}): Partial<PostHogConfig> {\n return {\n api_host: required(input.apiHost, 'apiHost'),\n ui_host: required(input.uiHost, 'uiHost'),\n defaults: POSTHOG_BROWSER_DEFAULTS,\n capture_exceptions: true,\n capture_pageview: 'history_change',\n custom_personal_data_properties: ['token'],\n mask_personal_data_properties: true,\n person_profiles: 'identified_only',\n session_recording: { maskAllInputs: true, blockSelector: '.ph-no-capture' },\n ...(input.tracingHostnames ? { tracing_headers: input.tracingHostnames } : {}),\n ...input.options,\n }\n}\n\nexport function postHogBrowserHeaders(client: { get_distinct_id(): string; get_session_id(): string | undefined }): Record<string, string> {\n const distinctId = postHogIdentifier(client.get_distinct_id())\n const sessionId = postHogIdentifier(client.get_session_id())\n return {\n ...(distinctId ? { [POSTHOG_DISTINCT_ID_HEADER]: distinctId } : {}),\n ...(sessionId ? { [POSTHOG_SESSION_ID_HEADER]: sessionId } : {}),\n }\n}\n\nfunction required(value: string, name: string) {\n const normalized = value.trim()\n if (!normalized) throw new Error(`${name} is required`)\n return normalized\n}\n"]}
|
package/dist/posthog/react.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { PostHogErrorBoundary, PostHogProvider, usePostHog } from '@posthog/react';
|
|
2
|
-
import { createElement, useEffect, useRef } from 'react';
|
|
2
|
+
import { createContext, createElement, useContext, useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { postHogBrowserOptions } from './client.js';
|
|
4
4
|
import { POSTHOG_DEFAULT_INGEST_PATH } from './proxy.js';
|
|
5
|
+
const PostHogLoadedContext = createContext(true);
|
|
5
6
|
export function PostHogIntegration({ children, environment, fallback = createElement('main', null, 'Something went wrong. Refresh the page to try again.'), ingestPath = POSTHOG_DEFAULT_INGEST_PATH, options, }) {
|
|
7
|
+
const [isLoaded, setIsLoaded] = useState(false);
|
|
8
|
+
const loadedRef = useRef(options?.loaded);
|
|
9
|
+
loadedRef.current = options?.loaded;
|
|
6
10
|
if (!environment)
|
|
7
11
|
return children;
|
|
8
12
|
const tracingHostnames = typeof window === 'undefined' ? undefined : [window.location.hostname];
|
|
@@ -12,29 +16,40 @@ export function PostHogIntegration({ children, environment, fallback = createEle
|
|
|
12
16
|
apiHost: ingestPath,
|
|
13
17
|
uiHost: environment.uiHost,
|
|
14
18
|
...(tracingHostnames ? { tracingHostnames } : {}),
|
|
15
|
-
|
|
19
|
+
options: {
|
|
20
|
+
...options,
|
|
21
|
+
loaded: (posthog) => {
|
|
22
|
+
setIsLoaded(true);
|
|
23
|
+
loadedRef.current?.(posthog);
|
|
24
|
+
},
|
|
25
|
+
},
|
|
16
26
|
}),
|
|
17
|
-
}, createElement(PostHogErrorBoundary, { fallback }, children));
|
|
27
|
+
}, createElement(PostHogLoadedContext.Provider, { value: isLoaded }, createElement(PostHogErrorBoundary, { fallback }, children)));
|
|
18
28
|
}
|
|
19
29
|
export function PostHogBetterAuthIdentity({ authClient, properties, }) {
|
|
20
30
|
const session = authClient.useSession();
|
|
21
31
|
const posthog = usePostHog();
|
|
32
|
+
const isLoaded = useContext(PostHogLoadedContext);
|
|
33
|
+
const isReady = isLoaded || posthog['__loaded'];
|
|
22
34
|
const identified = useRef(undefined);
|
|
23
35
|
const propertiesRef = useRef(properties);
|
|
24
36
|
propertiesRef.current = properties;
|
|
25
37
|
useEffect(() => {
|
|
26
|
-
if (session.isPending || session.error)
|
|
38
|
+
if (!isReady || session.isPending || session.error)
|
|
27
39
|
return;
|
|
28
40
|
const user = session.data?.user;
|
|
41
|
+
const persistedUserId = posthog.get_property('$user_id');
|
|
29
42
|
if (user) {
|
|
43
|
+
if (persistedUserId && persistedUserId !== user.id)
|
|
44
|
+
posthog.reset();
|
|
30
45
|
posthog.identify(user.id, propertiesRef.current?.(user));
|
|
31
46
|
identified.current = user.id;
|
|
32
47
|
}
|
|
33
|
-
else if (identified.current) {
|
|
48
|
+
else if (identified.current || persistedUserId) {
|
|
34
49
|
posthog.reset();
|
|
35
50
|
identified.current = undefined;
|
|
36
51
|
}
|
|
37
|
-
}, [posthog, session.data?.user, session.error, session.isPending]);
|
|
52
|
+
}, [isReady, posthog, session.data?.user, session.error, session.isPending]);
|
|
38
53
|
return null;
|
|
39
54
|
}
|
|
40
55
|
//# sourceMappingURL=react.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.js","sourceRoot":"","sources":["../../src/posthog/react.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAClF,OAAO,EAAE,aAAa,EAAkB,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"react.js","sourceRoot":"","sources":["../../src/posthog/react.ts"],"names":[],"mappings":"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","sourcesContent":["import { PostHogErrorBoundary, PostHogProvider, usePostHog } from '@posthog/react'\nimport { createContext, createElement, type ReactNode, useContext, useEffect, useRef, useState } from 'react'\nimport type { PostHogConfig, Properties } from 'posthog-js'\nimport { postHogBrowserOptions } from './client.js'\nimport type { PostHogEnvironment } from './config.js'\nimport { POSTHOG_DEFAULT_INGEST_PATH } from './proxy.js'\n\nconst PostHogLoadedContext = createContext(true)\n\nexport function PostHogIntegration({\n children,\n environment,\n fallback = createElement('main', null, 'Something went wrong. Refresh the page to try again.'),\n ingestPath = POSTHOG_DEFAULT_INGEST_PATH,\n options,\n}: {\n children?: ReactNode\n environment: PostHogEnvironment | undefined\n fallback?: ReactNode\n ingestPath?: string\n options?: Partial<PostHogConfig>\n}) {\n const [isLoaded, setIsLoaded] = useState(false)\n const loadedRef = useRef(options?.loaded)\n loadedRef.current = options?.loaded\n if (!environment) return children\n const tracingHostnames = typeof window === 'undefined' ? undefined : [window.location.hostname]\n return createElement(\n PostHogProvider,\n {\n apiKey: environment.projectToken,\n options: postHogBrowserOptions({\n apiHost: ingestPath,\n uiHost: environment.uiHost,\n ...(tracingHostnames ? { tracingHostnames } : {}),\n options: {\n ...options,\n loaded: (posthog) => {\n setIsLoaded(true)\n loadedRef.current?.(posthog)\n },\n },\n }),\n },\n createElement(PostHogLoadedContext.Provider, { value: isLoaded }, createElement(PostHogErrorBoundary, { fallback }, children)),\n )\n}\n\nexport type BetterAuthUser = { id: string }\nexport type BetterAuthSession<User extends BetterAuthUser = BetterAuthUser> = { user: User }\nexport type BetterAuthSessionState<User extends BetterAuthUser = BetterAuthUser> = {\n data?: BetterAuthSession<User> | null\n error?: unknown\n isPending: boolean\n}\nexport type BetterAuthReactClient<User extends BetterAuthUser = BetterAuthUser> = {\n useSession: () => BetterAuthSessionState<User>\n}\n\nexport function PostHogBetterAuthIdentity<User extends BetterAuthUser>({\n authClient,\n properties,\n}: {\n authClient: BetterAuthReactClient<User>\n properties?: (user: User) => Properties\n}) {\n const session = authClient.useSession()\n const posthog = usePostHog()\n const isLoaded = useContext(PostHogLoadedContext)\n const isReady = isLoaded || posthog['__loaded']\n const identified = useRef<string | undefined>(undefined)\n const propertiesRef = useRef(properties)\n propertiesRef.current = properties\n\n useEffect(() => {\n if (!isReady || session.isPending || session.error) return\n const user = session.data?.user\n const persistedUserId = posthog.get_property('$user_id')\n if (user) {\n if (persistedUserId && persistedUserId !== user.id) posthog.reset()\n posthog.identify(user.id, propertiesRef.current?.(user))\n identified.current = user.id\n } else if (identified.current || persistedUserId) {\n posthog.reset()\n identified.current = undefined\n }\n }, [isReady, posthog, session.data?.user, session.error, session.isPending])\n\n return null\n}\n"]}
|
|
@@ -52,6 +52,8 @@ describe('production auth flows', () => {
|
|
|
52
52
|
const resetLink = new URL(messageUrl(smtp.messages[1]!))
|
|
53
53
|
const token = resetLink.pathname.split('/').at(-1)
|
|
54
54
|
expect((await authRequest('/reset-password', { newPassword: 'new correct horse battery staple', token })).status).toBe(200)
|
|
55
|
+
expect(await currentUser(new Request('http://localhost:3100', { headers: { cookie: cookie! } }))).toBeUndefined()
|
|
56
|
+
expect((await authRequest('/reset-password', { newPassword: 'replayed password', token })).status).toBe(400)
|
|
55
57
|
expect((await authRequest('/sign-in/email', { email: 'ada@example.test', password: 'correct horse battery staple' })).status).toBe(401)
|
|
56
58
|
expect((await authRequest('/sign-in/email', { email: 'ada@example.test', password: 'new correct horse battery staple' })).status).toBe(
|
|
57
59
|
200,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { drizzleAdapter } from '@better-auth/drizzle-adapter'
|
|
2
2
|
import { betterAuth } from 'better-auth'
|
|
3
3
|
import { tanstackStartCookies } from 'better-auth/tanstack-start'
|
|
4
|
-
import { standardAccountOptions, standardRateLimitOptions, standardSessionOptions } from 'ras-stack/auth'
|
|
5
|
-
import type
|
|
4
|
+
import { standardAccountOptions, standardEmailAndPasswordOptions, standardRateLimitOptions, standardSessionOptions } from 'ras-stack/auth'
|
|
5
|
+
import { createAuthEmailHandler, type EmailDelivery } from 'ras-stack/email'
|
|
6
6
|
import type { AppEnvironment } from './environment'
|
|
7
7
|
import * as schema from './schema'
|
|
8
8
|
|
|
@@ -10,21 +10,20 @@ type Database = Parameters<typeof drizzleAdapter>[0]
|
|
|
10
10
|
|
|
11
11
|
export function createAuth(options: { database: Database; email?: EmailDelivery; environment: AppEnvironment; secret: string }) {
|
|
12
12
|
const email = options.email
|
|
13
|
-
const
|
|
14
|
-
? {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
: {}
|
|
13
|
+
const sendVerificationEmail = email
|
|
14
|
+
? createAuthEmailHandler(email, ({ user, url }) => ({
|
|
15
|
+
to: user.email,
|
|
16
|
+
subject: 'Verify your ras-stack example account',
|
|
17
|
+
text: `Verify your email: ${url}`,
|
|
18
|
+
}))
|
|
19
|
+
: undefined
|
|
20
|
+
const sendResetPassword = email
|
|
21
|
+
? createAuthEmailHandler(email, ({ user, url }) => ({
|
|
22
|
+
to: user.email,
|
|
23
|
+
subject: 'Reset your ras-stack example password',
|
|
24
|
+
text: `Reset your password: ${url}`,
|
|
25
|
+
}))
|
|
26
|
+
: undefined
|
|
28
27
|
return betterAuth({
|
|
29
28
|
appName: 'ras-stack full-stack example',
|
|
30
29
|
baseURL: options.environment.appUrl,
|
|
@@ -32,23 +31,19 @@ export function createAuth(options: { database: Database; email?: EmailDelivery;
|
|
|
32
31
|
trustedOrigins: [options.environment.appUrl],
|
|
33
32
|
database: drizzleAdapter(options.database, { provider: 'sqlite', schema }),
|
|
34
33
|
account: standardAccountOptions(),
|
|
35
|
-
emailAndPassword: {
|
|
36
|
-
enabled: true,
|
|
34
|
+
emailAndPassword: standardEmailAndPasswordOptions({
|
|
37
35
|
requireEmailVerification: Boolean(email) && options.environment.requireEmailVerification,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
: {}),
|
|
50
|
-
},
|
|
51
|
-
...mail,
|
|
36
|
+
...(sendResetPassword ? { sendResetPassword } : {}),
|
|
37
|
+
}),
|
|
38
|
+
...(sendVerificationEmail
|
|
39
|
+
? {
|
|
40
|
+
emailVerification: {
|
|
41
|
+
sendOnSignUp: true,
|
|
42
|
+
autoSignInAfterVerification: true,
|
|
43
|
+
sendVerificationEmail,
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
: {}),
|
|
52
47
|
session: standardSessionOptions(),
|
|
53
48
|
rateLimit: standardRateLimitOptions(),
|
|
54
49
|
advanced: {
|