@payez/next-mvp 4.14.0 → 4.15.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/CHANGELOG.md +23 -0
- package/dist/api-handlers/mfa/totp-confirm.d.ts +1 -1
- package/dist/api-handlers/mfa/totp-confirm.js +10 -6
- package/dist/api-handlers/test/force-expire.d.ts +4 -5
- package/dist/api-handlers/test/force-expire.js +14 -63
- package/dist/auth/better-auth.d.ts +16 -0
- package/dist/auth/better-auth.js +34 -0
- package/dist/lib/idp-client-config.js +1 -1
- package/dist/lib/log-safe.d.ts +6 -0
- package/dist/lib/log-safe.js +9 -0
- package/dist/lib/payez-client-secret.js +2 -1
- package/dist/lib/token-pages-switch.d.ts +23 -0
- package/dist/lib/token-pages-switch.js +48 -0
- package/dist/pages/test-env/JwtInspectPage.d.ts +16 -14
- package/dist/pages/test-env/JwtInspectPage.js +16 -14
- package/dist/pages/test-env/RefreshTokenPage.d.ts +7 -10
- package/dist/pages/test-env/RefreshTokenPage.js +7 -10
- package/dist/pages/test-env/guarded.js +21 -57
- package/dist/pages/test-env/index.d.ts +15 -24
- package/dist/pages/test-env/index.js +15 -26
- package/package.json +11 -1
- package/src/api-handlers/mfa/totp-confirm.ts +13 -5
- package/src/api-handlers/test/force-expire.ts +13 -70
- package/src/auth/better-auth.ts +44 -2
- package/src/lib/idp-client-config.ts +2 -2
- package/src/lib/log-safe.ts +9 -0
- package/src/lib/payez-client-secret.ts +2 -1
- package/src/lib/token-pages-switch.ts +44 -0
- package/src/pages/test-env/JwtInspectPage.tsx +16 -14
- package/src/pages/test-env/RefreshTokenPage.tsx +7 -10
- package/src/pages/test-env/guarded.tsx +21 -55
- package/src/pages/test-env/index.ts +15 -26
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog — @payez/next-mvp
|
|
2
2
|
|
|
3
|
+
## 4.15.0 (to be published under dist-tag `next` only, after 4.14.0; `latest` stays 4.8.9 until Jon says otherwise)
|
|
4
|
+
|
|
5
|
+
What main holds since 4.14.0, plus two new exports. Built from GitHub `keelsecurity/keelauth-next` main `1e65be6c9305`; this release commit changes only this file and the version. 4.14.0 was built from `33c188e0` (no tag, no `gitHead`); this one should be tagged `v4.15.0` at the sha it is published from. Consumers keep their own pins. The number is Jon's: two of the changes below are additive exports, which semver would call a minor.
|
|
6
|
+
|
|
7
|
+
**Behaviour changes (read first)**
|
|
8
|
+
|
|
9
|
+
- **CHANGE (PAY-3216, PR #25, Jon's order): the four test-env pages and the force-expire handler work with NO flag again, as before 4.2.0.** 4.14.0 still carried the `ENABLE_TEST_ROUTES` gate (`1d40a8c`: the pages rendered `notFound()` unless the flag was `true`; `072d20e`: force-expire answered 404 by default). Both gates are removed: `pages/test-env` renders each page unconditionally (the `guarded` barrel stays as the one seam; its only condition is PAY-3220's `PAYEZ_TOKEN_PAGES_OFF` for the two token pages, below, unset by default), and `api-handlers/test/force-expire` reaches its own session check (401 without a session) in development AND production. Its caller-supplied-token removal and the page redaction stay. **A consumer that mounts these pages or the force-expire route and relied on the flag to hide them in production must now gate them itself, or set `PAYEZ_TOKEN_PAGES_OFF` (next item) for the two token pages and force-expire.** Test: `test/pay-3216-test-env-no-flag.test.cjs`.
|
|
10
|
+
- **NEW SWITCH (PAY-3220, PR #33): `PAYEZ_TOKEN_PAGES_OFF`, UNSET = as today.** Read on the server per request (never at module scope, never `NEXT_PUBLIC_`, never from request input). `true`/`1`/`yes`/`on` (any case) makes `JwtInspectPage` and `RefreshTokenPage` answer `notFound()` (a real 404) and `POST /api/test/force-expire` answer an empty 404 before any session is read; `false`/`0`/`no`/`off` or unset/blank leaves them on; ANY OTHER value is OFF with one warning naming the variable. `TestEnvPage` and `EmergencyLogoutPage` are not covered. **Set it only on a production host, on Jon's word, together with the PAY-3220 production-roll note** (also unmount or gate the consumer's own `/api/internal/session-data`, which the jwt-inspect page reads, and leave `ENABLE_TOKEN_INSPECTOR` unset). Test: `test/pay-3220-token-pages-switch.test.cjs`.
|
|
11
|
+
- **FIX (PAY-3214, PR #26): `routes/mfa/totp-confirm` answers only `{ confirmed, stepped_up }`.** In 4.14.0 the re-minted access/refresh pair the IdP returns on a confirm reached the browser in the response body. It is now dropped: nothing is stored, the session's MFA state is untouched, and every 200 is that one projected body with `no-store`. A consumer that read tokens from this body (none should) gets none. Test: `test/pay-3214-totp-confirm-no-tokens.test.cjs`.
|
|
12
|
+
- **FIX (PAY-3139, PRs #17, #19): an enabled sign-in provider whose required credentials are missing is left out** (empty client id for any provider; empty client secret for google), with one `PROVIDER_CREDENTIALS_MISSING` console.error naming the provider and the missing FIELD, never a value. Its sign-in then answers better-auth's typed `PROVIDER_NOT_FOUND` instead of a zero-byte 500. The provider name in that line, and at the two other IdP-config log sites, passes through one shared `logSafeName` (`lib/log-safe`), so a name from the IdP's row cannot forge a second log line. Tests: `test/provider-credentials-missing.test.cjs` (8 rows, mutants red).
|
|
13
|
+
|
|
14
|
+
**New exports (additive)**
|
|
15
|
+
|
|
16
|
+
- **`./lib/idp-logout` (PAY-3245, PR #30):** `revokeSessionAtIdp(req)` and `IdpLogoutUnconfirmedError`, so a consumer with its own sign-out route calls the package's one IdP revoke instead of copying it. Test: `test/next-mvp-pay-3245-idp-logout-export.test.cjs`.
|
|
17
|
+
- **`./lib/first-enrol` (PAY-3270, PR #31):** `ENROL_COOKIE` (`'payez_enrol'`) and `ENROL_COOKIE_MAX_AGE` (`600`), so a consumer that parks the enrolment-scoped token for the first-enrol routes uses the package's cookie name instead of copying it. Test: `test/next-mvp-pay-3270-first-enrol-export.test.cjs`.
|
|
18
|
+
|
|
19
|
+
**Docs and tests only (no runtime change)**
|
|
20
|
+
|
|
21
|
+
- PAY-3228 B3 (PR #28): the five places that show `auth/login` say it is SERVER-ONLY and point a browser sign-in at `credential-login`.
|
|
22
|
+
- Tests: AUTH-045 provisioning half, `test/credential-login.test.cjs`: the first-sign-in journey is captured and a second enrolment is proven not to happen, with F1 as the red-first control and F2/F3 as positive controls (PR #12); AUTH-045 F1 asserts `enrolmentRequired` on the code turn (PR #23); C1 compares TypeScript diagnostic paths after `path.resolve` and has a positive control (PR #18); PAY-3214 M1's stub matchers accept either path separator; AUTH-045 R7b skips transient `__*mutant*` files.
|
|
23
|
+
|
|
24
|
+
**Not in this package:** `@payez/cli` 0.1.3 (pin on 4.14.0, PR #21) ships with `@payez/cli`.
|
|
25
|
+
|
|
3
26
|
## 4.14.0 (to be published under dist-tag `next` only, after 4.13.1; `latest` stays 4.8.9 until Jon says otherwise)
|
|
4
27
|
|
|
5
28
|
A minor: additive exports, plus one behaviour change in the generated-app credential sign-in (listed first). Built from GitHub `keelsecurity/keelauth-next` main `33c188e0`; this release commit changes only this file and the version. Consumers keep their own pins.
|
|
@@ -5,4 +5,4 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
5
5
|
* it carries the enrolment handle plus the code. Only after this succeeds is
|
|
6
6
|
* the method ACTIVE.
|
|
7
7
|
*/
|
|
8
|
-
export declare function POST(req: NextRequest): Promise<NextResponse<
|
|
8
|
+
export declare function POST(req: NextRequest): Promise<NextResponse<unknown>>;
|
|
@@ -36,11 +36,15 @@ async function POST(req) {
|
|
|
36
36
|
meta: { attemptedRefresh: result.attemptedRefresh },
|
|
37
37
|
}, { status: result.status, headers: mfa_json_guard_1.NO_STORE });
|
|
38
38
|
}
|
|
39
|
+
// PAY-3214: the browser gets ONLY `confirmed` and `stepped_up`. When the IdP steps the session up it
|
|
40
|
+
// re-mints and answers { confirmed, stepped_up: true, access_token, refresh_token, token_type }; relaying
|
|
41
|
+
// `data` whole put the person's bearer and refresh token in a browser-readable body. The pair is DROPPED
|
|
42
|
+
// here, not stored: storing it would change the session's MFA state from an ENROLMENT, which
|
|
43
|
+
// totp-verify.ts's header forbids, and that is a ruling not yet given (BAPert-MB 105226 s3-s4). With the
|
|
44
|
+
// pair dropped the session keeps its own token, as on the IdP's own `stepped_up: false` path.
|
|
45
|
+
// `stepped_up` is passed back as the IdP said it (its meaning, with the pair dropped, is s3 Q2 of that ruling).
|
|
39
46
|
const bodyJson = result.json;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return server_1.NextResponse.json(bodyJson, { status: 200, headers: mfa_json_guard_1.NO_STORE });
|
|
44
|
-
}
|
|
45
|
-
return server_1.NextResponse.json(bodyJson ?? {}, { status: 200, headers: mfa_json_guard_1.NO_STORE });
|
|
47
|
+
const enveloped = bodyJson && typeof bodyJson === 'object' && 'success' in bodyJson && 'data' in bodyJson;
|
|
48
|
+
const data = (enveloped ? (bodyJson.success === true ? bodyJson.data : null) : bodyJson) ?? {};
|
|
49
|
+
return server_1.NextResponse.json({ confirmed: data.confirmed === true, stepped_up: data.stepped_up === true }, { status: 200, headers: mfa_json_guard_1.NO_STORE });
|
|
46
50
|
}
|
|
@@ -15,19 +15,18 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
15
15
|
* mounts a destructive endpoint. Every one of the three surfaces this route
|
|
16
16
|
* ended up on followed exactly these instructions.
|
|
17
17
|
*
|
|
18
|
-
* The handler itself was fixed (244877:
|
|
19
|
-
*
|
|
18
|
+
* The handler itself was fixed (244877: the caller-supplied-token IDOR removed;
|
|
19
|
+
* its ENABLE_TEST_ROUTES gate was later removed again by PAY-3216). But a fixed handler shipped
|
|
20
20
|
* with unqualified mounting instructions still spreads: the guidance was never
|
|
21
21
|
* corrected alongside the code, and a package that documents a dangerous route
|
|
22
22
|
* as a plain convenience is instructing the weaker configuration.
|
|
23
23
|
*
|
|
24
24
|
* ── IF YOU MOUNT IT, MOUNT IT LIKE THIS ───────────────────────────────────
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* There is no env flag (PAY-3216): a mounted handler answers. Mount it only
|
|
26
|
+
* where you mean it to answer.
|
|
27
27
|
*
|
|
28
28
|
* ```typescript
|
|
29
29
|
* // app/api/test/force-expire/route.ts — TEST/DEV ENVIRONMENTS ONLY
|
|
30
|
-
* // Requires ENABLE_TEST_ROUTES=true in that environment; 404s otherwise.
|
|
31
30
|
* export { POST } from '@payez/next-mvp/api-handlers/test/force-expire';
|
|
32
31
|
* ```
|
|
33
32
|
*
|
|
@@ -6,6 +6,7 @@ const auth_1 = require("../../server/auth");
|
|
|
6
6
|
const session_store_1 = require("../../lib/session-store");
|
|
7
7
|
const safe_error_response_1 = require("../../lib/safe-error-response");
|
|
8
8
|
const token_log_id_1 = require("../../lib/token-log-id");
|
|
9
|
+
const token_pages_switch_1 = require("../../lib/token-pages-switch");
|
|
9
10
|
/**
|
|
10
11
|
* ⚠️ DESTRUCTIVE TEST-ONLY HANDLER — DO NOT MOUNT IN PRODUCTION. ⚠️
|
|
11
12
|
*
|
|
@@ -22,19 +23,18 @@ const token_log_id_1 = require("../../lib/token-log-id");
|
|
|
22
23
|
* mounts a destructive endpoint. Every one of the three surfaces this route
|
|
23
24
|
* ended up on followed exactly these instructions.
|
|
24
25
|
*
|
|
25
|
-
* The handler itself was fixed (244877:
|
|
26
|
-
*
|
|
26
|
+
* The handler itself was fixed (244877: the caller-supplied-token IDOR removed;
|
|
27
|
+
* its ENABLE_TEST_ROUTES gate was later removed again by PAY-3216). But a fixed handler shipped
|
|
27
28
|
* with unqualified mounting instructions still spreads: the guidance was never
|
|
28
29
|
* corrected alongside the code, and a package that documents a dangerous route
|
|
29
30
|
* as a plain convenience is instructing the weaker configuration.
|
|
30
31
|
*
|
|
31
32
|
* ── IF YOU MOUNT IT, MOUNT IT LIKE THIS ───────────────────────────────────
|
|
32
|
-
*
|
|
33
|
-
*
|
|
33
|
+
* There is no env flag (PAY-3216): a mounted handler answers. Mount it only
|
|
34
|
+
* where you mean it to answer.
|
|
34
35
|
*
|
|
35
36
|
* ```typescript
|
|
36
37
|
* // app/api/test/force-expire/route.ts — TEST/DEV ENVIRONMENTS ONLY
|
|
37
|
-
* // Requires ENABLE_TEST_ROUTES=true in that environment; 404s otherwise.
|
|
38
38
|
* export { POST } from '@payez/next-mvp/api-handlers/test/force-expire';
|
|
39
39
|
* ```
|
|
40
40
|
*
|
|
@@ -42,59 +42,12 @@ const token_log_id_1 = require("../../lib/token-log-id");
|
|
|
42
42
|
* The safest configuration is the absence of the route.
|
|
43
43
|
*/
|
|
44
44
|
const POST = async (req) => {
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// deployments on production hosts.
|
|
52
|
-
//
|
|
53
|
-
// The card was raised against one app's local copy. Sweeping the pattern
|
|
54
|
-
// rather than the instance found the same route in three places — a local
|
|
55
|
-
// copy in the graft, a local copy in idealvibe.online, and idealresume.online
|
|
56
|
-
// re-exporting THIS handler — so the fix belongs here, at the source all
|
|
57
|
-
// three ultimately derive from, not only in whichever copy was noticed first.
|
|
58
|
-
//
|
|
59
|
-
// The gate is the FIRST statement, before any session decode, so a disabled
|
|
60
|
-
// build does not reveal that the route is live. Absent, empty, or any value
|
|
61
|
-
// other than 'true' disables it. Distinguishable body on purpose: a bare 404
|
|
62
|
-
// reads as "route missing / build broken" and sends someone debugging
|
|
63
|
-
// deployment instead of reading a flag.
|
|
64
|
-
//
|
|
65
|
-
// A consumer that genuinely needs this in a test environment sets
|
|
66
|
-
// ENABLE_TEST_ROUTES=true there, which is the same posture the token
|
|
67
|
-
// inspector already uses for a strictly LESS dangerous, read-only route.
|
|
68
|
-
if (process.env.ENABLE_TEST_ROUTES !== 'true') {
|
|
69
|
-
// DISCLOSURE IS CONDITIONAL ON ENVIRONMENT (QAPert-NightHawk 34539, after
|
|
70
|
-
// QAPert caught that my first version shipped the very leak we had just
|
|
71
|
-
// criticised elsewhere).
|
|
72
|
-
//
|
|
73
|
-
// My original body named the flag: "Set ENABLE_TEST_ROUTES=true...". That
|
|
74
|
-
// is the same class of disclosure as TOKEN_INSPECTOR_DISABLED — it confirms
|
|
75
|
-
// the route exists, names the capability, and hands a prober the exact
|
|
76
|
-
// switch that turns it on. Having argued against that pattern one directory
|
|
77
|
-
// over, shipping it here would have been incoherent.
|
|
78
|
-
//
|
|
79
|
-
// Deleting the message outright is also wrong: it is what stops a developer
|
|
80
|
-
// losing an hour deciding whether their build is broken. The tension
|
|
81
|
-
// resolves by AUDIENCE, not by picking a side:
|
|
82
|
-
// production -> 404, EMPTY body. Nothing about the capability, the
|
|
83
|
-
// flag, or even that a handler is here.
|
|
84
|
-
// non-production -> 404 + the helpful message naming the flag.
|
|
85
|
-
//
|
|
86
|
-
// The operator affordance survives in production too, via the log line
|
|
87
|
-
// below: logs are visible to operators and not to probers, which is exactly
|
|
88
|
-
// the split the response body cannot make.
|
|
89
|
-
console.warn('[FORCE-EXPIRE] blocked: ENABLE_TEST_ROUTES is not "true" (test routes default-deny).');
|
|
90
|
-
if (process.env.NODE_ENV === 'production') {
|
|
91
|
-
return new server_1.NextResponse(null, { status: 404 });
|
|
92
|
-
}
|
|
93
|
-
return server_1.NextResponse.json({
|
|
94
|
-
error: 'TEST_ROUTES_DISABLED',
|
|
95
|
-
message: 'Test routes are disabled in this build. Set ENABLE_TEST_ROUTES=true to enable them (development only).',
|
|
96
|
-
}, { status: 404 });
|
|
97
|
-
}
|
|
45
|
+
// PAY-3216 (Jon's order, 15 Nov): the ENABLE_TEST_ROUTES default-deny gate that 072d20e (244877) put here is
|
|
46
|
+
// REMOVED. The handler works with no flag set, as before that commit. Mounting is the gate.
|
|
47
|
+
// PAY-3220 (ruled Win 105653 s1): the production switch for the token pages covers this route too, since the refresh
|
|
48
|
+
// page is its one caller. Unset, nothing changes. Set, an empty 404 before any session is read.
|
|
49
|
+
if ((0, token_pages_switch_1.tokenPagesOff)())
|
|
50
|
+
return new server_1.NextResponse(null, { status: 404 });
|
|
98
51
|
try {
|
|
99
52
|
const betterAuthSession = await (0, auth_1.getSession)(req);
|
|
100
53
|
// ACCESS CONTROL (QAPert 34553). This handler previously fell back to a
|
|
@@ -112,11 +65,9 @@ const POST = async (req) => {
|
|
|
112
65
|
// now ALWAYS the caller's own, which is also what the graft's local copy
|
|
113
66
|
// has always done.
|
|
114
67
|
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
// each other's sessions" is not acceptable there either — a control that
|
|
119
|
-
// only holds while a feature is switched off is not an access control.
|
|
68
|
+
// A control that only held while a feature was switched off would not be
|
|
69
|
+
// an access control: "unauthenticated users can expire each other's
|
|
70
|
+
// sessions" is not acceptable in any environment that mounts this.
|
|
120
71
|
//
|
|
121
72
|
// Corrects NextPert's own scope claim on card 244877 ("a caller could only
|
|
122
73
|
// ever expire their OWN session"): that was read off the graft's LOCAL
|
|
@@ -80,6 +80,22 @@ export interface IdpOAuthProviderConfig {
|
|
|
80
80
|
getUserInfo: ReturnType<typeof createIdpOAuthHooks>['getUserInfo'];
|
|
81
81
|
}
|
|
82
82
|
export declare function buildIdpOAuthProviderConfig(appBaseUrl?: string): IdpOAuthProviderConfig | null;
|
|
83
|
+
/**
|
|
84
|
+
* PAY-3139: the credentials better-auth ITSELF requires before it can build a provider's authorize URL.
|
|
85
|
+
*
|
|
86
|
+
* Read from @better-auth/core 1.5.6 (social-providers/*.mjs, createAuthorizationURL): google throws
|
|
87
|
+
* `BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED")` when clientId OR clientSecret is empty. That is a plain
|
|
88
|
+
* throw, not an APIError, so the sign-in door answered a 500 with an EMPTY body (dev-93, 14 Nov: the IdP served
|
|
89
|
+
* the public, secret-redacted client config, so google's secret arrived empty). apple, facebook and github have
|
|
90
|
+
* no such guard (facebook signs in by id_token; apple's secret is the IdP's), so only the id is required there:
|
|
91
|
+
* an empty client id cannot produce a working authorize URL for any provider.
|
|
92
|
+
*
|
|
93
|
+
* Returns the missing FIELD NAMES, never a value.
|
|
94
|
+
*/
|
|
95
|
+
export declare function missingProviderCredentials(name: string, oauth: {
|
|
96
|
+
clientId?: string | null;
|
|
97
|
+
clientSecret?: string | null;
|
|
98
|
+
}): Array<'clientId' | 'clientSecret'>;
|
|
83
99
|
export declare function buildBetterAuthProviders(config: IDPClientConfig): Record<string, BetterAuthSocialProvider>;
|
|
84
100
|
/**
|
|
85
101
|
* Optional configuration for `createBetterAuthInstance`.
|
package/dist/auth/better-auth.js
CHANGED
|
@@ -46,6 +46,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
46
46
|
exports.IDP_ERROR_CODES_TO_FORWARD = exports.OAUTH_SIGN_IN_FAILED_ERROR = void 0;
|
|
47
47
|
exports.secretFingerprint = secretFingerprint;
|
|
48
48
|
exports.buildIdpOAuthProviderConfig = buildIdpOAuthProviderConfig;
|
|
49
|
+
exports.missingProviderCredentials = missingProviderCredentials;
|
|
49
50
|
exports.buildBetterAuthProviders = buildBetterAuthProviders;
|
|
50
51
|
exports.createBetterAuthInstance = createBetterAuthInstance;
|
|
51
52
|
exports.isBetterAuthEnabled = isBetterAuthEnabled;
|
|
@@ -241,12 +242,45 @@ function buildIdpOAuthProviderConfig(appBaseUrl) {
|
|
|
241
242
|
*/
|
|
242
243
|
};
|
|
243
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* PAY-3139: the credentials better-auth ITSELF requires before it can build a provider's authorize URL.
|
|
247
|
+
*
|
|
248
|
+
* Read from @better-auth/core 1.5.6 (social-providers/*.mjs, createAuthorizationURL): google throws
|
|
249
|
+
* `BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED")` when clientId OR clientSecret is empty. That is a plain
|
|
250
|
+
* throw, not an APIError, so the sign-in door answered a 500 with an EMPTY body (dev-93, 14 Nov: the IdP served
|
|
251
|
+
* the public, secret-redacted client config, so google's secret arrived empty). apple, facebook and github have
|
|
252
|
+
* no such guard (facebook signs in by id_token; apple's secret is the IdP's), so only the id is required there:
|
|
253
|
+
* an empty client id cannot produce a working authorize URL for any provider.
|
|
254
|
+
*
|
|
255
|
+
* Returns the missing FIELD NAMES, never a value.
|
|
256
|
+
*/
|
|
257
|
+
function missingProviderCredentials(name, oauth) {
|
|
258
|
+
const required = name === 'google' ? ['clientId', 'clientSecret'] : ['clientId'];
|
|
259
|
+
return required.filter((field) => String(oauth[field] ?? '').trim() === '');
|
|
260
|
+
}
|
|
244
261
|
function buildBetterAuthProviders(config) {
|
|
245
262
|
const providers = {};
|
|
246
263
|
for (const oauth of config.oauthProviders || []) {
|
|
247
264
|
if (!oauth.enabled)
|
|
248
265
|
continue;
|
|
249
266
|
const name = oauth.provider.toLowerCase();
|
|
267
|
+
/**
|
|
268
|
+
* PAY-3139: an ENABLED row that lacks a credential better-auth requires is LEFT OUT, loudly. Left in, its
|
|
269
|
+
* sign-in throws inside better-auth and the door answers an empty 500 that names nothing. Left out, the
|
|
270
|
+
* same door answers better-auth's typed 404 PROVIDER_NOT_FOUND, and this line names the provider and the
|
|
271
|
+
* missing field (never a value), once per instance build.
|
|
272
|
+
*/
|
|
273
|
+
const missing = missingProviderCredentials(name, oauth);
|
|
274
|
+
if (missing.length > 0) {
|
|
275
|
+
const secretHint = missing.includes('clientSecret')
|
|
276
|
+
? ` (the secret is read env-first from ${(0, idp_client_config_1.providerSecretEnvVar)(oauth.provider)}, else from the IdP's ` +
|
|
277
|
+
`secret-bearing client config, which a client that sends no valid client_secret does not receive)`
|
|
278
|
+
: '';
|
|
279
|
+
console.error(`[BETTER_AUTH] PROVIDER_CREDENTIALS_MISSING provider=${(0, log_safe_1.logSafeName)(name)} missing=${missing.join(',')}: ` +
|
|
280
|
+
`left out of the sign-in providers, so its sign-in answers PROVIDER_NOT_FOUND until the ` +
|
|
281
|
+
`${missing.join(' and ')} reach this app${secretHint}`);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
250
284
|
const additionalParams = oauth.additionalParams ?? {};
|
|
251
285
|
const rawPrompt = additionalParams.prompt;
|
|
252
286
|
const rawAccessType = additionalParams.accessType ?? additionalParams.access_type;
|
|
@@ -162,7 +162,7 @@ function applyEnvSecrets(config) {
|
|
|
162
162
|
const envVar = providerSecretEnvVar(p.provider);
|
|
163
163
|
const fromEnv = process.env[envVar];
|
|
164
164
|
const source = fromEnv ? `env:${envVar}` : p.clientSecret ? 'idp-config' : 'none';
|
|
165
|
-
logSecretSource(`${p.provider} clientSecret`, source);
|
|
165
|
+
logSecretSource(`${(0, log_safe_1.logSafeName)(p.provider)} clientSecret`, source);
|
|
166
166
|
const clientSecret = fromEnv || p.clientSecret;
|
|
167
167
|
(0, never_in_response_1.markNeverInResponse)(clientSecret);
|
|
168
168
|
return { ...p, clientSecret };
|
package/dist/lib/log-safe.d.ts
CHANGED
|
@@ -17,3 +17,9 @@ export declare function closedCodeForLog(code: unknown): string | null;
|
|
|
17
17
|
* `NonError`; a status outside 100-599 is dropped; a code outside the closed token shape is dropped.
|
|
18
18
|
*/
|
|
19
19
|
export declare function errForLog(err: unknown): string;
|
|
20
|
+
/**
|
|
21
|
+
* A provider name (or any other IdP-supplied identifier) for a log line: lower-cased, then anything outside [a-z0-9_-] becomes '?',
|
|
22
|
+
* so a name carrying a newline cannot forge a second log line (Nextpert-Scout 102543 LOW 1; the shared helper per 104626, used at
|
|
23
|
+
* every site that logs an IdP provider name: better-auth.ts, payez-client-secret.ts, idp-client-config.ts).
|
|
24
|
+
*/
|
|
25
|
+
export declare function logSafeName(name: unknown): string;
|
package/dist/lib/log-safe.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
14
|
exports.closedCodeForLog = closedCodeForLog;
|
|
15
15
|
exports.errForLog = errForLog;
|
|
16
|
+
exports.logSafeName = logSafeName;
|
|
16
17
|
/** An upstream `code` is loggable only if it is a closed token (UPPER_SNAKE). Anything else, including free text, becomes null. */
|
|
17
18
|
function closedCodeForLog(code) {
|
|
18
19
|
return typeof code === 'string' && /^[A-Z0-9_]{1,64}$/.test(code) ? code : null;
|
|
@@ -30,3 +31,11 @@ function errForLog(err) {
|
|
|
30
31
|
const code = closed ? ` code=${closed}` : '';
|
|
31
32
|
return `error=${name}${status}${code}`;
|
|
32
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* A provider name (or any other IdP-supplied identifier) for a log line: lower-cased, then anything outside [a-z0-9_-] becomes '?',
|
|
36
|
+
* so a name carrying a newline cannot forge a second log line (Nextpert-Scout 102543 LOW 1; the shared helper per 104626, used at
|
|
37
|
+
* every site that logs an IdP provider name: better-auth.ts, payez-client-secret.ts, idp-client-config.ts).
|
|
38
|
+
*/
|
|
39
|
+
function logSafeName(name) {
|
|
40
|
+
return String(name ?? '').toLowerCase().replace(/[^a-z0-9_-]/g, '?');
|
|
41
|
+
}
|
|
@@ -10,6 +10,7 @@ exports.warnIfProviderSecretRedacted = warnIfProviderSecretRedacted;
|
|
|
10
10
|
* what it sent before. The value is never logged and never leaves in a response (markNeverInResponse).
|
|
11
11
|
*/
|
|
12
12
|
const never_in_response_1 = require("./never-in-response");
|
|
13
|
+
const log_safe_1 = require("./log-safe");
|
|
13
14
|
exports.CLIENT_SECRET_ENV = 'PAYEZ_CLIENT_SECRET';
|
|
14
15
|
/** `{ client_secret }` when the env var is set to a non-blank value, else `{}`. Spread it into a JSON body. */
|
|
15
16
|
function clientSecretBody() {
|
|
@@ -36,6 +37,6 @@ function warnIfProviderSecretRedacted(redactedHeader, providers, envVarFor) {
|
|
|
36
37
|
if (warnedProviders.has(p.provider))
|
|
37
38
|
continue;
|
|
38
39
|
warnedProviders.add(p.provider);
|
|
39
|
-
console.warn(`[IDP_CONFIG] PAYEZ_PROVIDER_SECRET_MISSING_REDACTED provider=${p.provider}`);
|
|
40
|
+
console.warn(`[IDP_CONFIG] PAYEZ_PROVIDER_SECRET_MISSING_REDACTED provider=${(0, log_safe_1.logSafeName)(p.provider)}`);
|
|
40
41
|
}
|
|
41
42
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAY-3220 (ruled Win 105653 s1, on NextPert-MB's Mac 105619): the ONE production switch for the two token-showing
|
|
3
|
+
* test-env pages (JwtInspectPage, RefreshTokenPage) and the route the refresh page drives (POST /api/test/force-expire).
|
|
4
|
+
*
|
|
5
|
+
* PAYEZ_TOKEN_PAGES_OFF unset or empty -> ON (every page works as today; the default everywhere)
|
|
6
|
+
* true / 1 / yes / on -> OFF (any letter case)
|
|
7
|
+
* false / 0 / no / off -> ON (any letter case), no warning
|
|
8
|
+
* anything else -> OFF, and ONE warning per process naming the variable
|
|
9
|
+
*
|
|
10
|
+
* Why an unrecognised value is OFF (the ruling's one change to the recommendation): nobody sets this variable except to
|
|
11
|
+
* turn the pages off, on Jon's word, on a production host. A mistyped attempt to turn them off must not leave
|
|
12
|
+
* token-showing pages on in production with only a log line to say so.
|
|
13
|
+
*
|
|
14
|
+
* Read from the server's process environment on every call, never captured at module scope (a module constant would be
|
|
15
|
+
* whatever the build host had). It is not NEXT_PUBLIC_, so it never reaches a client bundle, and no request input
|
|
16
|
+
* (cookie, header, query string, body, session) is consulted: only whoever can change the host's environment can turn it.
|
|
17
|
+
*
|
|
18
|
+
* Set ONLY on a production host, ONLY on Jon's word, on the day production is real. It is never in a tracked .env file.
|
|
19
|
+
* The switch is not counted as set until the production roll's served check reads both pages and force-expire back as
|
|
20
|
+
* 404 by request (BAPert-MB 105629 s3).
|
|
21
|
+
*/
|
|
22
|
+
export declare const TOKEN_PAGES_OFF_ENV = "PAYEZ_TOKEN_PAGES_OFF";
|
|
23
|
+
export declare function tokenPagesOff(): boolean;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TOKEN_PAGES_OFF_ENV = void 0;
|
|
4
|
+
exports.tokenPagesOff = tokenPagesOff;
|
|
5
|
+
/**
|
|
6
|
+
* PAY-3220 (ruled Win 105653 s1, on NextPert-MB's Mac 105619): the ONE production switch for the two token-showing
|
|
7
|
+
* test-env pages (JwtInspectPage, RefreshTokenPage) and the route the refresh page drives (POST /api/test/force-expire).
|
|
8
|
+
*
|
|
9
|
+
* PAYEZ_TOKEN_PAGES_OFF unset or empty -> ON (every page works as today; the default everywhere)
|
|
10
|
+
* true / 1 / yes / on -> OFF (any letter case)
|
|
11
|
+
* false / 0 / no / off -> ON (any letter case), no warning
|
|
12
|
+
* anything else -> OFF, and ONE warning per process naming the variable
|
|
13
|
+
*
|
|
14
|
+
* Why an unrecognised value is OFF (the ruling's one change to the recommendation): nobody sets this variable except to
|
|
15
|
+
* turn the pages off, on Jon's word, on a production host. A mistyped attempt to turn them off must not leave
|
|
16
|
+
* token-showing pages on in production with only a log line to say so.
|
|
17
|
+
*
|
|
18
|
+
* Read from the server's process environment on every call, never captured at module scope (a module constant would be
|
|
19
|
+
* whatever the build host had). It is not NEXT_PUBLIC_, so it never reaches a client bundle, and no request input
|
|
20
|
+
* (cookie, header, query string, body, session) is consulted: only whoever can change the host's environment can turn it.
|
|
21
|
+
*
|
|
22
|
+
* Set ONLY on a production host, ONLY on Jon's word, on the day production is real. It is never in a tracked .env file.
|
|
23
|
+
* The switch is not counted as set until the production roll's served check reads both pages and force-expire back as
|
|
24
|
+
* 404 by request (BAPert-MB 105629 s3).
|
|
25
|
+
*/
|
|
26
|
+
exports.TOKEN_PAGES_OFF_ENV = 'PAYEZ_TOKEN_PAGES_OFF';
|
|
27
|
+
const OFF_WORDS = new Set(['true', '1', 'yes', 'on']);
|
|
28
|
+
const ON_WORDS = new Set(['false', '0', 'no', 'off']);
|
|
29
|
+
let warnedUnrecognised = false;
|
|
30
|
+
function tokenPagesOff() {
|
|
31
|
+
const raw = process.env[exports.TOKEN_PAGES_OFF_ENV];
|
|
32
|
+
if (raw === undefined)
|
|
33
|
+
return false;
|
|
34
|
+
const value = raw.trim().toLowerCase();
|
|
35
|
+
if (value === '')
|
|
36
|
+
return false;
|
|
37
|
+
if (OFF_WORDS.has(value))
|
|
38
|
+
return true;
|
|
39
|
+
if (ON_WORDS.has(value))
|
|
40
|
+
return false;
|
|
41
|
+
if (!warnedUnrecognised) {
|
|
42
|
+
warnedUnrecognised = true;
|
|
43
|
+
// The variable's NAME only. The value is not a secret, but nothing about it helps the reader of this line.
|
|
44
|
+
console.warn(`[test-env] ${exports.TOKEN_PAGES_OFF_ENV} is set to a value that is not recognised (true/1/yes/on or false/0/no/off); ` +
|
|
45
|
+
'the token pages are OFF: jwt-inspect, refresh-token and POST /api/test/force-expire answer 404.');
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
@@ -3,23 +3,25 @@
|
|
|
3
3
|
* IN PRODUCTION. ⚠️
|
|
4
4
|
*
|
|
5
5
|
* Session Inspector Page. Debug page for inspecting session data from Redis:
|
|
6
|
-
* shows user info, roles, 2FA status, and
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* shows user info, roles, 2FA status, and the DECODED CLAIMS of the IDP access
|
|
7
|
+
* token, which it reads from the consuming app's `/api/internal/session-data`
|
|
8
|
+
* route. Credentials themselves are shown as tokenLogId + length ONLY: the
|
|
9
|
+
* Copy-to-clipboard and Reveal controls this docstring used to describe were
|
|
10
|
+
* removed by PAY-1470 (BAPert 59022, 59040), and test-env-no-token-material
|
|
11
|
+
* holds the page to that. (PAY-3220: this sentence was stale until now.)
|
|
9
12
|
*
|
|
10
13
|
* ────────────────────────── WHY THIS WARNING EXISTS (QAPert 35000 / 248991) ──────────────────────────
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Like the force-expire handler (no flag since PAY-3216) and unlike the
|
|
15
|
+
* idp-token route (behind ENABLE_TOKEN_INSPECTOR), a mounted page is reachable
|
|
16
|
+
* exactly as far as the consuming app's own middleware allows. MOUNTING IT IS
|
|
17
|
+
* THE GATE. The one switch on it is PAYEZ_TOKEN_PAGES_OFF (PAY-3220), unset by
|
|
18
|
+
* default and set only on a production host on Jon's word.
|
|
16
19
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* Treat the rendered value as a live credential, because that is what it is.
|
|
20
|
+
* WHAT IT STILL HANDS OVER IS SENSITIVE. The decoded claims say who the
|
|
21
|
+
* signed-in person is, which roles they hold and how they authenticated, and
|
|
22
|
+
* the 246007 finding reduces to "a caller who KNOWS A SESSION TOKEN can act on
|
|
23
|
+
* a session it does not own": a page that once showed that token is the reason
|
|
24
|
+
* this one is treated as a production hazard.
|
|
23
25
|
*
|
|
24
26
|
* NOTE FOR MAINTAINERS: this warning must stay on THIS docstring, the one
|
|
25
27
|
* attached to the exported declaration. An identical block placed higher in
|
|
@@ -39,23 +39,25 @@ function decodeJwtPayload(token) {
|
|
|
39
39
|
* IN PRODUCTION. ⚠️
|
|
40
40
|
*
|
|
41
41
|
* Session Inspector Page. Debug page for inspecting session data from Redis:
|
|
42
|
-
* shows user info, roles, 2FA status, and
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* shows user info, roles, 2FA status, and the DECODED CLAIMS of the IDP access
|
|
43
|
+
* token, which it reads from the consuming app's `/api/internal/session-data`
|
|
44
|
+
* route. Credentials themselves are shown as tokenLogId + length ONLY: the
|
|
45
|
+
* Copy-to-clipboard and Reveal controls this docstring used to describe were
|
|
46
|
+
* removed by PAY-1470 (BAPert 59022, 59040), and test-env-no-token-material
|
|
47
|
+
* holds the page to that. (PAY-3220: this sentence was stale until now.)
|
|
45
48
|
*
|
|
46
49
|
* ────────────────────────── WHY THIS WARNING EXISTS (QAPert 35000 / 248991) ──────────────────────────
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
50
|
+
* Like the force-expire handler (no flag since PAY-3216) and unlike the
|
|
51
|
+
* idp-token route (behind ENABLE_TOKEN_INSPECTOR), a mounted page is reachable
|
|
52
|
+
* exactly as far as the consuming app's own middleware allows. MOUNTING IT IS
|
|
53
|
+
* THE GATE. The one switch on it is PAYEZ_TOKEN_PAGES_OFF (PAY-3220), unset by
|
|
54
|
+
* default and set only on a production host on Jon's word.
|
|
52
55
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* Treat the rendered value as a live credential, because that is what it is.
|
|
56
|
+
* WHAT IT STILL HANDS OVER IS SENSITIVE. The decoded claims say who the
|
|
57
|
+
* signed-in person is, which roles they hold and how they authenticated, and
|
|
58
|
+
* the 246007 finding reduces to "a caller who KNOWS A SESSION TOKEN can act on
|
|
59
|
+
* a session it does not own": a page that once showed that token is the reason
|
|
60
|
+
* this one is treated as a production hazard.
|
|
59
61
|
*
|
|
60
62
|
* NOTE FOR MAINTAINERS: this warning must stay on THIS docstring, the one
|
|
61
63
|
* attached to the exported declaration. An identical block placed higher in
|
|
@@ -7,25 +7,22 @@
|
|
|
7
7
|
* `/api/test/force-expire`. That endpoint MUTATES SESSION STATE.
|
|
8
8
|
*
|
|
9
9
|
* ────────────────────────── WHY THIS WARNING EXISTS (QAPert 35000) ──────────────────────────
|
|
10
|
-
* The force-expire HANDLER was fixed (244877
|
|
11
|
-
*
|
|
12
|
-
* covered the ROUTE AND NOT THE PAGE THAT DRIVES IT. This page is a separate
|
|
10
|
+
* The force-expire HANDLER was fixed (244877) and its docstring corrected to
|
|
11
|
+
* say so — but that fix covered the ROUTE AND NOT THE PAGE THAT DRIVES IT. This page is a separate
|
|
13
12
|
* public export carrying its own mount instructions, so a consumer can follow
|
|
14
13
|
* these lines and never once read the handler's warning.
|
|
15
14
|
*
|
|
16
|
-
* THIS PAGE HAS NO ENV GATE OF ITS OWN
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* setting that flag anywhere this page is also reachable.
|
|
15
|
+
* THIS PAGE HAS NO ENV GATE OF ITS OWN, and since PAY-3216 neither has the
|
|
16
|
+
* handler it drives: mounting is the gate. Wherever this page and the
|
|
17
|
+
* force-expire route are both mounted, the button works.
|
|
20
18
|
*
|
|
21
19
|
* ────────────────────────── IF YOU MOUNT IT, MOUNT IT LIKE THIS ──────────────────────────
|
|
22
20
|
* ```typescript
|
|
23
21
|
* // app/test-env/refresh-token/page.tsx — TEST/DEV ENVIRONMENTS ONLY
|
|
24
22
|
* export { RefreshTokenPage as default } from '@payez/next-mvp/pages/test-env';
|
|
25
23
|
* ```
|
|
26
|
-
*
|
|
27
|
-
* do
|
|
28
|
-
* refresh flow, DO NOT MOUNT THIS AT ALL.
|
|
24
|
+
* The button needs /api/test/force-expire mounted too; there is no flag. If
|
|
25
|
+
* you do not need to exercise the refresh flow, DO NOT MOUNT THIS AT ALL.
|
|
29
26
|
*/
|
|
30
27
|
export declare function RefreshTokenPage(): import("react/jsx-runtime").JSX.Element;
|
|
31
28
|
export default RefreshTokenPage;
|
|
@@ -17,25 +17,22 @@ const redact_credentials_1 = require("./redact-credentials");
|
|
|
17
17
|
* `/api/test/force-expire`. That endpoint MUTATES SESSION STATE.
|
|
18
18
|
*
|
|
19
19
|
* ────────────────────────── WHY THIS WARNING EXISTS (QAPert 35000) ──────────────────────────
|
|
20
|
-
* The force-expire HANDLER was fixed (244877
|
|
21
|
-
*
|
|
22
|
-
* covered the ROUTE AND NOT THE PAGE THAT DRIVES IT. This page is a separate
|
|
20
|
+
* The force-expire HANDLER was fixed (244877) and its docstring corrected to
|
|
21
|
+
* say so — but that fix covered the ROUTE AND NOT THE PAGE THAT DRIVES IT. This page is a separate
|
|
23
22
|
* public export carrying its own mount instructions, so a consumer can follow
|
|
24
23
|
* these lines and never once read the handler's warning.
|
|
25
24
|
*
|
|
26
|
-
* THIS PAGE HAS NO ENV GATE OF ITS OWN
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* setting that flag anywhere this page is also reachable.
|
|
25
|
+
* THIS PAGE HAS NO ENV GATE OF ITS OWN, and since PAY-3216 neither has the
|
|
26
|
+
* handler it drives: mounting is the gate. Wherever this page and the
|
|
27
|
+
* force-expire route are both mounted, the button works.
|
|
30
28
|
*
|
|
31
29
|
* ────────────────────────── IF YOU MOUNT IT, MOUNT IT LIKE THIS ──────────────────────────
|
|
32
30
|
* ```typescript
|
|
33
31
|
* // app/test-env/refresh-token/page.tsx — TEST/DEV ENVIRONMENTS ONLY
|
|
34
32
|
* export { RefreshTokenPage as default } from '@payez/next-mvp/pages/test-env';
|
|
35
33
|
* ```
|
|
36
|
-
*
|
|
37
|
-
* do
|
|
38
|
-
* refresh flow, DO NOT MOUNT THIS AT ALL.
|
|
34
|
+
* The button needs /api/test/force-expire mounted too; there is no flag. If
|
|
35
|
+
* you do not need to exercise the refresh flow, DO NOT MOUNT THIS AT ALL.
|
|
39
36
|
*/
|
|
40
37
|
function RefreshTokenPage() {
|
|
41
38
|
const { data: session } = better_auth_client_1.authClient.useSession();
|