@biffo/cli 0.77.0 → 0.78.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.
|
@@ -22,14 +22,38 @@ function userWithSession(session: unknown, err: Error | null = null) {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// The env vars are the TRANSITIONAL fallback (#403 Stage 3 removes them). The
|
|
26
|
+
// document is now the preferred source, so most tests set env AND control
|
|
27
|
+
// `fetch` to prove which one wins.
|
|
25
28
|
function configureCognitoEnv() {
|
|
26
29
|
vi.stubEnv('NEXT_PUBLIC_CORE_COGNITO_USER_POOL_ID', 'us-east-1_TESTPOOL')
|
|
27
30
|
vi.stubEnv('NEXT_PUBLIC_CORE_COGNITO_CLIENT_ID', 'testclientid')
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
+
// A `fetch` returning a valid identity document. The ids differ from the env
|
|
34
|
+
// values on purpose so a test can prove the DOCUMENT wins over baked env.
|
|
35
|
+
function mockFetchDocument(overrides: Record<string, unknown> = {}) {
|
|
36
|
+
const body = {
|
|
37
|
+
userPoolId: 'us-east-1_DOCPOOL',
|
|
38
|
+
clientId: 'docclientid',
|
|
39
|
+
region: 'us-east-1',
|
|
40
|
+
...overrides,
|
|
41
|
+
}
|
|
42
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
43
|
+
ok: true,
|
|
44
|
+
json: async () => body,
|
|
45
|
+
}) as unknown as typeof fetch
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// A `fetch` that rejects (network error) — drives the env fallback path.
|
|
49
|
+
function mockFetchUnreachable() {
|
|
50
|
+
global.fetch = vi.fn().mockRejectedValue(new Error('network down')) as unknown as typeof fetch
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Each test re-imports the module fresh: both the pool (auth.ts) and the
|
|
54
|
+
// resolved-identity Promise (identity.ts) are memoised at module scope, so a
|
|
55
|
+
// module instance left over from a previous test would carry that test's
|
|
56
|
+
// document/env into the next one.
|
|
33
57
|
async function loadAuth() {
|
|
34
58
|
vi.resetModules()
|
|
35
59
|
return await import('@/lib/auth')
|
|
@@ -39,10 +63,15 @@ describe('getCurrentSession', () => {
|
|
|
39
63
|
beforeEach(() => {
|
|
40
64
|
vi.clearAllMocks()
|
|
41
65
|
configureCognitoEnv()
|
|
66
|
+
// Default: document unreachable, so these behaviour tests build the pool
|
|
67
|
+
// from the env fallback (us-east-1_TESTPOOL / testclientid) exactly as the
|
|
68
|
+
// pre-runtime version did.
|
|
69
|
+
mockFetchUnreachable()
|
|
42
70
|
})
|
|
43
71
|
|
|
44
72
|
afterEach(() => {
|
|
45
73
|
vi.unstubAllEnvs()
|
|
74
|
+
vi.restoreAllMocks()
|
|
46
75
|
})
|
|
47
76
|
|
|
48
77
|
it('returns null when no user is stored in the shared localStorage session', async () => {
|
|
@@ -71,18 +100,113 @@ describe('getCurrentSession', () => {
|
|
|
71
100
|
})
|
|
72
101
|
})
|
|
73
102
|
|
|
103
|
+
// The core now publishes its Cognito coordinates at runtime; the sibling
|
|
104
|
+
// prefers that document over its baked env vars (#403/#400). These tests pin
|
|
105
|
+
// which source wins under each condition.
|
|
106
|
+
describe('runtime core identity resolution', () => {
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
vi.clearAllMocks()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
vi.unstubAllEnvs()
|
|
113
|
+
vi.restoreAllMocks()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('builds the pool from the published document, not from env', async () => {
|
|
117
|
+
// Env is set to DIFFERENT values so a pass proves the document won.
|
|
118
|
+
configureCognitoEnv()
|
|
119
|
+
mockFetchDocument()
|
|
120
|
+
const { getCurrentSession } = await loadAuth()
|
|
121
|
+
getCurrentUser.mockReturnValue(null)
|
|
122
|
+
|
|
123
|
+
await getCurrentSession()
|
|
124
|
+
|
|
125
|
+
expect(global.fetch).toHaveBeenCalledWith('/.well-known/biffo-identity.json', {
|
|
126
|
+
cache: 'no-store',
|
|
127
|
+
})
|
|
128
|
+
expect(poolConstructor).toHaveBeenCalledWith({
|
|
129
|
+
UserPoolId: 'us-east-1_DOCPOOL',
|
|
130
|
+
ClientId: 'docclientid',
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('falls back to env and warns when the document is unreachable', async () => {
|
|
135
|
+
configureCognitoEnv()
|
|
136
|
+
mockFetchUnreachable()
|
|
137
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
138
|
+
const { getCurrentSession } = await loadAuth()
|
|
139
|
+
getCurrentUser.mockReturnValue(null)
|
|
140
|
+
|
|
141
|
+
await getCurrentSession()
|
|
142
|
+
|
|
143
|
+
expect(poolConstructor).toHaveBeenCalledWith({
|
|
144
|
+
UserPoolId: 'us-east-1_TESTPOOL',
|
|
145
|
+
ClientId: 'testclientid',
|
|
146
|
+
})
|
|
147
|
+
expect(warn).toHaveBeenCalledOnce()
|
|
148
|
+
expect(warn.mock.calls[0]?.[0]).toContain('DEGRADED')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('falls back to env when the document is served but missing ids', async () => {
|
|
152
|
+
configureCognitoEnv()
|
|
153
|
+
// ok:true but the body lacks a clientId — treated as unusable.
|
|
154
|
+
mockFetchDocument({ clientId: '' })
|
|
155
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
156
|
+
const { getCurrentSession } = await loadAuth()
|
|
157
|
+
getCurrentUser.mockReturnValue(null)
|
|
158
|
+
|
|
159
|
+
await getCurrentSession()
|
|
160
|
+
|
|
161
|
+
expect(poolConstructor).toHaveBeenCalledWith({
|
|
162
|
+
UserPoolId: 'us-east-1_TESTPOOL',
|
|
163
|
+
ClientId: 'testclientid',
|
|
164
|
+
})
|
|
165
|
+
expect(warn).toHaveBeenCalledOnce()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('resolves null and never constructs the pool when both document and env are absent', async () => {
|
|
169
|
+
vi.stubEnv('NEXT_PUBLIC_CORE_COGNITO_USER_POOL_ID', '')
|
|
170
|
+
vi.stubEnv('NEXT_PUBLIC_CORE_COGNITO_CLIENT_ID', '')
|
|
171
|
+
mockFetchUnreachable()
|
|
172
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
173
|
+
const { getCurrentSession } = await loadAuth()
|
|
174
|
+
|
|
175
|
+
await expect(getCurrentSession()).resolves.toBeNull()
|
|
176
|
+
expect(poolConstructor).not.toHaveBeenCalled()
|
|
177
|
+
// No document AND no env is an unconfigured build, not degradation — quiet.
|
|
178
|
+
expect(warn).not.toHaveBeenCalled()
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('fetches the document at most once across multiple session reads (memoised)', async () => {
|
|
182
|
+
configureCognitoEnv()
|
|
183
|
+
mockFetchDocument()
|
|
184
|
+
const { getCurrentSession } = await loadAuth()
|
|
185
|
+
getCurrentUser.mockReturnValue(null)
|
|
186
|
+
|
|
187
|
+
await getCurrentSession()
|
|
188
|
+
await getCurrentSession()
|
|
189
|
+
await getCurrentSession()
|
|
190
|
+
|
|
191
|
+
expect(global.fetch).toHaveBeenCalledTimes(1)
|
|
192
|
+
expect(poolConstructor).toHaveBeenCalledTimes(1)
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
74
196
|
// The CognitoUserPool constructor throws ("Both UserPoolId and ClientId are
|
|
75
197
|
// required") when either value is missing, and `next build` prerenders `/` in
|
|
76
198
|
// Node — which imports this module. Building the pool at module scope therefore
|
|
77
|
-
// made the skeleton un-buildable without
|
|
78
|
-
//
|
|
199
|
+
// made the skeleton un-buildable without a resolvable identity in scope. These
|
|
200
|
+
// tests pin that shut.
|
|
79
201
|
describe('lazy pool construction', () => {
|
|
80
202
|
beforeEach(() => {
|
|
81
203
|
vi.clearAllMocks()
|
|
204
|
+
mockFetchUnreachable()
|
|
82
205
|
})
|
|
83
206
|
|
|
84
207
|
afterEach(() => {
|
|
85
208
|
vi.unstubAllEnvs()
|
|
209
|
+
vi.restoreAllMocks()
|
|
86
210
|
})
|
|
87
211
|
|
|
88
212
|
it('does not construct the pool merely by importing the module', async () => {
|
|
@@ -91,11 +215,19 @@ describe('lazy pool construction', () => {
|
|
|
91
215
|
expect(poolConstructor).not.toHaveBeenCalled()
|
|
92
216
|
})
|
|
93
217
|
|
|
94
|
-
it('imports cleanly with no Cognito env vars set', async () => {
|
|
218
|
+
it('imports cleanly with no Cognito env vars set and no fetch available', async () => {
|
|
95
219
|
vi.stubEnv('NEXT_PUBLIC_CORE_COGNITO_USER_POOL_ID', '')
|
|
96
220
|
vi.stubEnv('NEXT_PUBLIC_CORE_COGNITO_CLIENT_ID', '')
|
|
97
|
-
|
|
98
|
-
|
|
221
|
+
// Simulate `next build` prerendering `/` in Node with no fetch in scope.
|
|
222
|
+
const originalFetch = global.fetch
|
|
223
|
+
// @ts-expect-error deliberately remove fetch to mimic the Node prerender.
|
|
224
|
+
delete global.fetch
|
|
225
|
+
try {
|
|
226
|
+
await expect(loadAuth()).resolves.toBeDefined()
|
|
227
|
+
expect(poolConstructor).not.toHaveBeenCalled()
|
|
228
|
+
} finally {
|
|
229
|
+
global.fetch = originalFetch
|
|
230
|
+
}
|
|
99
231
|
})
|
|
100
232
|
|
|
101
233
|
it('resolves null instead of throwing when Cognito env vars are absent', async () => {
|
|
@@ -106,7 +238,7 @@ describe('lazy pool construction', () => {
|
|
|
106
238
|
expect(poolConstructor).not.toHaveBeenCalled()
|
|
107
239
|
})
|
|
108
240
|
|
|
109
|
-
it('constructs the pool once, on first session read, from the
|
|
241
|
+
it('constructs the pool once, on first session read, from the env fallback', async () => {
|
|
110
242
|
configureCognitoEnv()
|
|
111
243
|
const { getCurrentSession } = await loadAuth()
|
|
112
244
|
getCurrentUser.mockReturnValue(null)
|
|
@@ -127,6 +259,9 @@ describe('lazy pool construction', () => {
|
|
|
127
259
|
// login path that bypasses the portal and silently breaks single-sign-on.
|
|
128
260
|
// If you are here because you deliberately added one of these, that decision
|
|
129
261
|
// belongs in an ADR — not in a green test suite.
|
|
262
|
+
//
|
|
263
|
+
// resolveCoreIdentity lives in identity.ts, deliberately NOT re-exported here:
|
|
264
|
+
// auth.ts's surface stays exactly one export.
|
|
130
265
|
describe('sibling auth surface', () => {
|
|
131
266
|
it('exposes only session reading — no sign-in/sign-out machinery', async () => {
|
|
132
267
|
const authModule = await loadAuth()
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
type ICognitoUserPoolData,
|
|
5
5
|
} from 'amazon-cognito-identity-js'
|
|
6
6
|
|
|
7
|
+
import { resolveCoreIdentity } from './identity'
|
|
8
|
+
|
|
7
9
|
// ---------------------------------------------------------------------------
|
|
8
10
|
// SHARED-SESSION INVARIANT — read this before changing anything below.
|
|
9
11
|
//
|
|
@@ -29,28 +31,38 @@ import {
|
|
|
29
31
|
// portal too.
|
|
30
32
|
// ---------------------------------------------------------------------------
|
|
31
33
|
// Module-private on purpose: the pool is an implementation detail of
|
|
32
|
-
// getCurrentSession(), not part of this sibling's auth surface.
|
|
34
|
+
// getCurrentSession(), not part of this sibling's auth surface. Its Cognito
|
|
35
|
+
// coordinates now come from resolveCoreIdentity() (identity.ts), which prefers
|
|
36
|
+
// the core's runtime-published /.well-known/biffo-identity.json document and
|
|
37
|
+
// only falls back to the baked NEXT_PUBLIC_CORE_COGNITO_* env vars when that
|
|
38
|
+
// document is unreachable — see identity.ts for why runtime resolution kills
|
|
39
|
+
// the stale-baked-pool bug class (#403/#400).
|
|
33
40
|
//
|
|
34
41
|
// Constructed LAZILY, on first session read — never at module scope. The
|
|
35
42
|
// CognitoUserPool constructor throws ("Both UserPoolId and ClientId are
|
|
36
43
|
// required") when either value is missing, and `next build` prerenders `/` in
|
|
37
44
|
// Node, which imports this module. Constructing eagerly therefore made the
|
|
38
|
-
// whole app un-buildable without
|
|
39
|
-
//
|
|
45
|
+
// whole app un-buildable without a resolvable identity in scope — a build is
|
|
46
|
+
// not a sign-in, and it has no business needing pool credentials. Deferring
|
|
40
47
|
// keeps `pnpm run build` (and any import of this module) working with no env
|
|
41
|
-
//
|
|
42
|
-
// surfaces as "signed out" rather than a hard crash.
|
|
48
|
+
// and no reachable document, while an actual session read in a misconfigured
|
|
49
|
+
// deployment surfaces as "signed out" rather than a hard crash.
|
|
50
|
+
//
|
|
51
|
+
// The pool itself is memoised: resolveCoreIdentity() is memoised too, but the
|
|
52
|
+
// CognitoUserPool wrapper is built here exactly once so repeated session reads
|
|
53
|
+
// reuse one instance (and its localStorage view of the shared session).
|
|
43
54
|
let userPool: CognitoUserPool | null = null
|
|
44
55
|
|
|
45
|
-
function getUserPool(): CognitoUserPool | null {
|
|
56
|
+
async function getUserPool(): Promise<CognitoUserPool | null> {
|
|
46
57
|
if (userPool) return userPool
|
|
47
58
|
|
|
59
|
+
const identity = await resolveCoreIdentity()
|
|
60
|
+
if (!identity) return null
|
|
61
|
+
|
|
48
62
|
const poolData: ICognitoUserPoolData = {
|
|
49
|
-
UserPoolId:
|
|
50
|
-
ClientId:
|
|
63
|
+
UserPoolId: identity.userPoolId,
|
|
64
|
+
ClientId: identity.clientId,
|
|
51
65
|
}
|
|
52
|
-
if (!poolData.UserPoolId || !poolData.ClientId) return null
|
|
53
|
-
|
|
54
66
|
userPool = new CognitoUserPool(poolData)
|
|
55
67
|
return userPool
|
|
56
68
|
}
|
|
@@ -62,15 +74,15 @@ function getUserPool(): CognitoUserPool | null {
|
|
|
62
74
|
* (see `createApiClient`, which takes a `getIdToken` callback). A null result
|
|
63
75
|
* means "redirect the user to the portal's login".
|
|
64
76
|
*/
|
|
65
|
-
export function getCurrentSession(): Promise<CognitoUserSession | null> {
|
|
77
|
+
export async function getCurrentSession(): Promise<CognitoUserSession | null> {
|
|
78
|
+
// Resolving the pool is now async because the core identity is fetched at
|
|
79
|
+
// runtime (identity.ts). A null pool means no resolvable core Cognito config,
|
|
80
|
+
// which is indistinguishable from "not signed in" as far as callers are
|
|
81
|
+
// concerned — the semantics are unchanged from the env-only version.
|
|
82
|
+
const pool = await getUserPool()
|
|
83
|
+
if (!pool) return null
|
|
84
|
+
|
|
66
85
|
return new Promise((resolve) => {
|
|
67
|
-
// No pool means no core Cognito config, which is indistinguishable from
|
|
68
|
-
// "not signed in" as far as callers are concerned.
|
|
69
|
-
const pool = getUserPool()
|
|
70
|
-
if (!pool) {
|
|
71
|
-
resolve(null)
|
|
72
|
-
return
|
|
73
|
-
}
|
|
74
86
|
const user = pool.getCurrentUser()
|
|
75
87
|
if (!user) {
|
|
76
88
|
resolve(null)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// RUNTIME CORE IDENTITY (#403 / #400)
|
|
3
|
+
//
|
|
4
|
+
// The core project publishes its Cognito coordinates at
|
|
5
|
+
// `/.well-known/biffo-identity.json`, served same-origin from the portal
|
|
6
|
+
// bucket (see modules/cloud/aws/cdn/main.tf and .github/workflows/deploy-infra.yml).
|
|
7
|
+
// This module resolves that document at RUNTIME so a sibling never has to bake
|
|
8
|
+
// the core's User Pool / App Client id into its bundle.
|
|
9
|
+
//
|
|
10
|
+
// WHY runtime, not build-time:
|
|
11
|
+
// Baking `NEXT_PUBLIC_CORE_COGNITO_*` into the static export copies a
|
|
12
|
+
// snapshot of the core's pool id into every sibling. When the core replaces
|
|
13
|
+
// its pool (or client), every sibling still points at the dead one until it
|
|
14
|
+
// is rebuilt and redeployed — that stranding is exactly the bug class #400
|
|
15
|
+
// and #403 exist to delete. Reading the published document each page load
|
|
16
|
+
// removes the copy entirely: the source of truth lives with the core.
|
|
17
|
+
//
|
|
18
|
+
// WHY a RELATIVE, same-origin fetch is valid:
|
|
19
|
+
// A sibling and the core portal live on the SAME ORIGIN (baseurl.com/ vs
|
|
20
|
+
// baseurl.com/<name>/). That is the same property that makes shared-session
|
|
21
|
+
// SSO work (see the SHARED-SESSION INVARIANT in auth.ts). So a relative
|
|
22
|
+
// `fetch('/.well-known/biffo-identity.json')` reaches the core's document
|
|
23
|
+
// with no CORS, no configured base URL, and no knowledge of the deployment's
|
|
24
|
+
// domain.
|
|
25
|
+
//
|
|
26
|
+
// WHY memoised:
|
|
27
|
+
// The document is immutable for a page's lifetime, and auth is read on many
|
|
28
|
+
// code paths. Memoising the in-flight/resolved Promise at module scope makes
|
|
29
|
+
// this exactly ONE network request per page load, shared by every caller,
|
|
30
|
+
// rather than one per session read.
|
|
31
|
+
//
|
|
32
|
+
// The env fallback below is TRANSITIONAL (Stage 3 of #403 removes it). It lets
|
|
33
|
+
// an instance whose core has not yet started publishing the document keep
|
|
34
|
+
// working off its baked env vars, so instances can upgrade at their own pace.
|
|
35
|
+
// A baked value that shadows a live document is precisely the stale-copy bug
|
|
36
|
+
// we are removing, so falling back because the document was UNREACHABLE is a
|
|
37
|
+
// degraded path and warns loudly.
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
export interface CoreIdentity {
|
|
41
|
+
userPoolId: string
|
|
42
|
+
clientId: string
|
|
43
|
+
region?: string
|
|
44
|
+
apiUrl?: string
|
|
45
|
+
portalUrl?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The relative, same-origin path the core publishes its identity document at.
|
|
49
|
+
const IDENTITY_DOCUMENT_PATH = '/.well-known/biffo-identity.json'
|
|
50
|
+
|
|
51
|
+
// Memoised for the page's lifetime: the first call kicks off the fetch and
|
|
52
|
+
// every subsequent call shares the same Promise, so the document is requested
|
|
53
|
+
// at most once. Storing the Promise (not the resolved value) means concurrent
|
|
54
|
+
// callers before the fetch settles also coalesce onto the one request.
|
|
55
|
+
let cached: Promise<CoreIdentity | null> | null = null
|
|
56
|
+
|
|
57
|
+
// Read the transitional env fallback. Returns a valid CoreIdentity only when
|
|
58
|
+
// BOTH ids are present; otherwise null (an unconfigured build has neither a
|
|
59
|
+
// document nor env, and that is not an error to shout about).
|
|
60
|
+
function identityFromEnv(): CoreIdentity | null {
|
|
61
|
+
const userPoolId = process.env['NEXT_PUBLIC_CORE_COGNITO_USER_POOL_ID'] ?? ''
|
|
62
|
+
const clientId = process.env['NEXT_PUBLIC_CORE_COGNITO_CLIENT_ID'] ?? ''
|
|
63
|
+
if (!userPoolId || !clientId) return null
|
|
64
|
+
return { userPoolId, clientId }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// A parsed document counts only when it carries both ids non-empty; a document
|
|
68
|
+
// missing either is treated as unusable and triggers the fallback.
|
|
69
|
+
function identityFromDocument(data: unknown): CoreIdentity | null {
|
|
70
|
+
if (typeof data !== 'object' || data === null) return null
|
|
71
|
+
const doc = data as Record<string, unknown>
|
|
72
|
+
const userPoolId = typeof doc['userPoolId'] === 'string' ? doc['userPoolId'] : ''
|
|
73
|
+
const clientId = typeof doc['clientId'] === 'string' ? doc['clientId'] : ''
|
|
74
|
+
if (!userPoolId || !clientId) return null
|
|
75
|
+
|
|
76
|
+
const identity: CoreIdentity = { userPoolId, clientId }
|
|
77
|
+
if (typeof doc['region'] === 'string') identity.region = doc['region']
|
|
78
|
+
if (typeof doc['apiUrl'] === 'string') identity.apiUrl = doc['apiUrl']
|
|
79
|
+
if (typeof doc['portalUrl'] === 'string') identity.portalUrl = doc['portalUrl']
|
|
80
|
+
return identity
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function fetchCoreIdentity(): Promise<CoreIdentity | null> {
|
|
84
|
+
try {
|
|
85
|
+
// `no-store`: never let a stale document linger in the fetch cache — the
|
|
86
|
+
// whole point of runtime resolution is to always see the core's current
|
|
87
|
+
// coordinates.
|
|
88
|
+
const res = await fetch(IDENTITY_DOCUMENT_PATH, { cache: 'no-store' })
|
|
89
|
+
if (res.ok) {
|
|
90
|
+
const identity = identityFromDocument(await res.json())
|
|
91
|
+
if (identity) return identity
|
|
92
|
+
}
|
|
93
|
+
// Reached here => the document was served but unusable (non-ok status or
|
|
94
|
+
// missing ids). Fall through to the degraded env fallback below.
|
|
95
|
+
} catch {
|
|
96
|
+
// Network error, or `fetch` not available (e.g. `next build` prerendering
|
|
97
|
+
// `/` in Node with no fetch). Fall through to the env fallback.
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// DEGRADED path: the document was unreachable/unusable, so we lean on the
|
|
101
|
+
// baked env vars. If those exist, warn once — a stale baked value silently
|
|
102
|
+
// shadowing a live document is the #403/#400 bug. If they DON'T exist, this
|
|
103
|
+
// is simply an unconfigured build (no document, no env) — not degradation,
|
|
104
|
+
// so stay quiet.
|
|
105
|
+
const fallback = identityFromEnv()
|
|
106
|
+
if (fallback) {
|
|
107
|
+
console.warn(
|
|
108
|
+
'[biffo] DEGRADED: could not resolve the core identity document at ' +
|
|
109
|
+
`${IDENTITY_DOCUMENT_PATH}; falling back to baked ` +
|
|
110
|
+
'NEXT_PUBLIC_CORE_COGNITO_* env vars. This is transitional (#403) and ' +
|
|
111
|
+
'risks pointing at a stale/dead Cognito pool — ensure the core is ' +
|
|
112
|
+
'publishing the runtime identity document.',
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
return fallback
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Resolve the core's Cognito identity at runtime, preferring the published
|
|
120
|
+
* `/.well-known/biffo-identity.json` document and falling back to baked
|
|
121
|
+
* `NEXT_PUBLIC_CORE_COGNITO_*` env vars when the document is unreachable.
|
|
122
|
+
*
|
|
123
|
+
* Memoised: at most one fetch per page load, shared by every caller. Returns
|
|
124
|
+
* null when neither the document nor the env vars supply both ids.
|
|
125
|
+
*/
|
|
126
|
+
export function resolveCoreIdentity(): Promise<CoreIdentity | null> {
|
|
127
|
+
cached ??= fetchCoreIdentity()
|
|
128
|
+
return cached
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Test-only: clear the memoised resolution so the next `resolveCoreIdentity()`
|
|
133
|
+
* fetches afresh. Production code never calls this — the memo is meant to live
|
|
134
|
+
* for the whole page. Tests that re-import the module via `vi.resetModules()`
|
|
135
|
+
* get a fresh module-scope `cached` for free; this hook is for tests that want
|
|
136
|
+
* to reset within a single module instance.
|
|
137
|
+
*/
|
|
138
|
+
export function __resetCoreIdentityForTests(): void {
|
|
139
|
+
cached = null
|
|
140
|
+
}
|