@duffcloudservices/cms 0.13.1 → 0.13.2
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/dist/{chunk-HVSF23P7.js → chunk-64BUBTW7.js} +148 -24
- package/dist/chunk-64BUBTW7.js.map +1 -0
- package/dist/{chunk-A5F4C72F.js → chunk-OGAJX4KM.js} +2 -2
- package/dist/{chunk-A5F4C72F.js.map → chunk-OGAJX4KM.js.map} +1 -1
- package/dist/index.d.ts +130 -5
- package/dist/index.js +48 -17
- package/dist/index.js.map +1 -1
- package/dist/{installSeoHead-kWQwObez.d.ts → installSeoHead-EE0Z7UmK.d.ts} +60 -9
- package/dist/plugins/index.d.ts +2 -2
- package/dist/plugins/index.js +2 -2
- package/dist/seo/index.d.ts +139 -4
- package/dist/seo/index.js +2 -2
- package/dist/{vitepressTransform-JG_zlaux.d.ts → vitepressTransform-Ds5Hff8t.d.ts} +29 -5
- package/package.json +2 -2
- package/src/components/ResponsiveImage.test.ts +45 -0
- package/src/components/ResponsiveImage.vue +12 -0
- package/src/composables/anonymousSessionGate.test.ts +243 -0
- package/src/composables/useSiteVisitorSession.ts +100 -20
- package/dist/chunk-HVSF23P7.js.map +0 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
readSiteVisitorSessionResponse,
|
|
4
|
+
siteVisitorFromSessionPayload,
|
|
5
|
+
useSiteVisitorSession,
|
|
6
|
+
} from './useSiteVisitorSession'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* THE A-35 DEFECT CLASS — "any 2xx == authenticated".
|
|
10
|
+
*
|
|
11
|
+
* `GET /api/v1/site-auth/session` answers an ANONYMOUS visitor with
|
|
12
|
+
* `200 { "visitor": null }` (server commit a384d3a15, 2026-07-03, A-35) so a signed-out
|
|
13
|
+
* page load does not log a red console error. Any gate that reads the STATUS instead of
|
|
14
|
+
* the PAYLOAD therefore reports "authenticated" for every anonymous visitor, and the
|
|
15
|
+
* member-gated fetches it guards fire and 401 — once per page view.
|
|
16
|
+
*
|
|
17
|
+
* That is not hypothetical: kept's hand-rolled `useVisitorStore().checkAuth()` did exactly
|
|
18
|
+
* this and produced 346 profile-401s in a single day on kineticenergypt.com, a 1.00 ratio
|
|
19
|
+
* to session-200s on EVERY day observed (C-619, kept commit 85ba35c).
|
|
20
|
+
*
|
|
21
|
+
* The shared composable was already payload-derived — but it OWNS its own fetch, so a site
|
|
22
|
+
* with an existing store (Pinia, caching, in-flight dedup, its own `apiFetch`) could not
|
|
23
|
+
* adopt it and re-derived `return true`. These tests pin the payload-level seams that such
|
|
24
|
+
* a store CAN adopt, so the defect class cannot recur on the next site that grows member
|
|
25
|
+
* features.
|
|
26
|
+
*
|
|
27
|
+
* Every case here is stated as: what the caller may conclude from a given response. The
|
|
28
|
+
* only response that may ever conclude "authenticated" is one carrying a real visitor.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
function installConsoleSpies() {
|
|
32
|
+
return {
|
|
33
|
+
error: vi.spyOn(console, 'error').mockImplementation(() => {}),
|
|
34
|
+
warn: vi.spyOn(console, 'warn').mockImplementation(() => {}),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function expectSilent(spies: ReturnType<typeof installConsoleSpies>) {
|
|
39
|
+
expect(spies.error).not.toHaveBeenCalled()
|
|
40
|
+
expect(spies.warn).not.toHaveBeenCalled()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A minimal `Response` double: what a hand-rolled store's own `fetch` hands back. */
|
|
44
|
+
function jsonResponse(status: number, body: unknown): Response {
|
|
45
|
+
return {
|
|
46
|
+
ok: status >= 200 && status < 300,
|
|
47
|
+
status,
|
|
48
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
49
|
+
clone: () => jsonResponse(status, body),
|
|
50
|
+
text: async () => JSON.stringify(body),
|
|
51
|
+
json: async () => body,
|
|
52
|
+
} as unknown as Response
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
vi.unstubAllGlobals()
|
|
57
|
+
vi.restoreAllMocks()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('siteVisitorFromSessionPayload — the payload is the only source of truth', () => {
|
|
61
|
+
it('the anonymous 200 body {visitor:null} yields no visitor', () => {
|
|
62
|
+
expect(siteVisitorFromSessionPayload({ visitor: null })).toBeNull()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('a MISSING visitor key yields null, never a truthy undefined', () => {
|
|
66
|
+
// kept's pre-fix store assigned `data.visitor` raw, so a missing key stored
|
|
67
|
+
// `undefined` — falsy by luck, and truthy under any `!== null` test written later.
|
|
68
|
+
expect(siteVisitorFromSessionPayload({})).toBeNull()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('a server claiming authenticated:true with NO visitor still yields null', () => {
|
|
72
|
+
// Fail closed: the envelope's own boolean is not evidence of a visitor.
|
|
73
|
+
expect(siteVisitorFromSessionPayload({ authenticated: true, visitor: null })).toBeNull()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('a visitor object with no email is not a visitor', () => {
|
|
77
|
+
expect(siteVisitorFromSessionPayload({ visitor: { id: 'g-1' } })).toBeNull()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('non-object bodies yield null', () => {
|
|
81
|
+
expect(siteVisitorFromSessionPayload(null)).toBeNull()
|
|
82
|
+
expect(siteVisitorFromSessionPayload(undefined)).toBeNull()
|
|
83
|
+
expect(siteVisitorFromSessionPayload('ok')).toBeNull()
|
|
84
|
+
expect(siteVisitorFromSessionPayload(true)).toBeNull()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('a real visitor is returned, normalized', () => {
|
|
88
|
+
const v = siteVisitorFromSessionPayload({
|
|
89
|
+
visitor: { id: 'g-1', email: 'v@example.com', name: 'Visitor' },
|
|
90
|
+
})
|
|
91
|
+
expect(v?.email).toBe('v@example.com')
|
|
92
|
+
expect(v?.name).toBe('Visitor')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('the contracts-spec {authenticated,user} shape is accepted too', () => {
|
|
96
|
+
expect(siteVisitorFromSessionPayload({ authenticated: true, user: { email: 'u@example.com' } })?.email).toBe(
|
|
97
|
+
'u@example.com',
|
|
98
|
+
)
|
|
99
|
+
// name defaults to the email rather than inventing one.
|
|
100
|
+
expect(siteVisitorFromSessionPayload({ user: { email: 'u@example.com' } })?.name).toBe('u@example.com')
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe('readSiteVisitorSessionResponse — status alone can NEVER conclude authenticated', () => {
|
|
105
|
+
it('THE REGRESSION PIN: 200 {visitor:null} is NOT authenticated', async () => {
|
|
106
|
+
const spies = installConsoleSpies()
|
|
107
|
+
|
|
108
|
+
const result = await readSiteVisitorSessionResponse(jsonResponse(200, { visitor: null }))
|
|
109
|
+
|
|
110
|
+
// A status-only gate (`return response.ok`) passes every other test in this file
|
|
111
|
+
// but fails this one. That is the whole point.
|
|
112
|
+
expect(result.authenticated).toBe(false)
|
|
113
|
+
expect(result.visitor).toBeNull()
|
|
114
|
+
expect(result.apiUnreachable).toBeFalsy()
|
|
115
|
+
expectSilent(spies)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('200 with a missing visitor key is NOT authenticated', async () => {
|
|
119
|
+
const result = await readSiteVisitorSessionResponse(jsonResponse(200, {}))
|
|
120
|
+
expect(result.authenticated).toBe(false)
|
|
121
|
+
expect(result.visitor).toBeNull()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('200 with a real visitor IS authenticated', async () => {
|
|
125
|
+
const result = await readSiteVisitorSessionResponse(
|
|
126
|
+
jsonResponse(200, { visitor: { id: 'g-1', email: 'v@example.com', name: 'Visitor' } }),
|
|
127
|
+
)
|
|
128
|
+
expect(result.authenticated).toBe(true)
|
|
129
|
+
expect(result.visitor?.email).toBe('v@example.com')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('a legacy 401 is not authenticated, and is silent (the anonymous path on old servers)', async () => {
|
|
133
|
+
const spies = installConsoleSpies()
|
|
134
|
+
const result = await readSiteVisitorSessionResponse(jsonResponse(401, { error: 'Not authenticated' }))
|
|
135
|
+
expect(result.authenticated).toBe(false)
|
|
136
|
+
expect(result.visitor).toBeNull()
|
|
137
|
+
expectSilent(spies)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('a 500 is not authenticated — fail closed, never fail open', async () => {
|
|
141
|
+
const result = await readSiteVisitorSessionResponse(jsonResponse(500, { error: 'boom' }))
|
|
142
|
+
expect(result.authenticated).toBe(false)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('a non-2xx that somehow carries a visitor is STILL not authenticated', async () => {
|
|
146
|
+
// Fail closed: a rejected status is a rejection whatever the body says.
|
|
147
|
+
const result = await readSiteVisitorSessionResponse(
|
|
148
|
+
jsonResponse(401, { visitor: { email: 'v@example.com', name: 'V' } }),
|
|
149
|
+
)
|
|
150
|
+
expect(result.authenticated).toBe(false)
|
|
151
|
+
expect(result.visitor).toBeNull()
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('a missing response (the callers fetch threw) is not authenticated, and silent', async () => {
|
|
155
|
+
const spies = installConsoleSpies()
|
|
156
|
+
expect((await readSiteVisitorSessionResponse(null)).authenticated).toBe(false)
|
|
157
|
+
expect((await readSiteVisitorSessionResponse(undefined)).authenticated).toBe(false)
|
|
158
|
+
expectSilent(spies)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('a body that will not parse is not authenticated, and silent', async () => {
|
|
162
|
+
const broken = {
|
|
163
|
+
ok: true,
|
|
164
|
+
status: 200,
|
|
165
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
166
|
+
clone: () => broken,
|
|
167
|
+
text: async () => '',
|
|
168
|
+
json: async () => {
|
|
169
|
+
throw new Error('connection reset')
|
|
170
|
+
},
|
|
171
|
+
} as unknown as Response
|
|
172
|
+
const spies = installConsoleSpies()
|
|
173
|
+
|
|
174
|
+
const result = await readSiteVisitorSessionResponse(broken)
|
|
175
|
+
|
|
176
|
+
expect(result.authenticated).toBe(false)
|
|
177
|
+
expect(result.apiUnreachable).toBeFalsy()
|
|
178
|
+
expectSilent(spies)
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
describe('readSiteVisitorSessionResponse — a misroute is not an anonymous visit (C-298 layer 2)', () => {
|
|
183
|
+
const SPA_SHELL =
|
|
184
|
+
'<!DOCTYPE html><html><head><script type="module" src="/assets/index.js"></script></head><body><div id="app"></div></body></html>'
|
|
185
|
+
|
|
186
|
+
function htmlResponse(): Response {
|
|
187
|
+
const build = (): Response =>
|
|
188
|
+
({
|
|
189
|
+
ok: true,
|
|
190
|
+
status: 200,
|
|
191
|
+
headers: new Headers({ 'content-type': 'text/html; charset=utf-8' }),
|
|
192
|
+
clone: () => build(),
|
|
193
|
+
text: async () => SPA_SHELL,
|
|
194
|
+
json: async () => JSON.parse(SPA_SHELL),
|
|
195
|
+
}) as unknown as Response
|
|
196
|
+
return build()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
it('200 text/html is signed-out for the UI, apiUnreachable, and LOUD', async () => {
|
|
200
|
+
const spies = installConsoleSpies()
|
|
201
|
+
|
|
202
|
+
const result = await readSiteVisitorSessionResponse(htmlResponse())
|
|
203
|
+
|
|
204
|
+
expect(result.authenticated).toBe(false)
|
|
205
|
+
expect(result.apiUnreachable).toBe(true)
|
|
206
|
+
expect(spies.error).toHaveBeenCalledTimes(1)
|
|
207
|
+
const message = String(spies.error.mock.calls[0][0])
|
|
208
|
+
expect(message).toContain('bodyClass=spa-html')
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
describe('useSiteVisitorSession — consumers inherit the payload gate without opting in', () => {
|
|
213
|
+
it('isAuthenticated stays false across an anonymous 200-null probe', async () => {
|
|
214
|
+
vi.stubGlobal(
|
|
215
|
+
'fetch',
|
|
216
|
+
vi.fn().mockResolvedValue({ ok: true, status: 200, json: async () => ({ visitor: null }) }),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
const session = useSiteVisitorSession({ fetchOnMount: false })
|
|
220
|
+
expect(session.isAuthenticated.value).toBe(false)
|
|
221
|
+
await session.refresh()
|
|
222
|
+
|
|
223
|
+
expect(session.isAuthenticated.value).toBe(false)
|
|
224
|
+
expect(session.visitor.value).toBeNull()
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('isAuthenticated flips true only when a visitor is present', async () => {
|
|
228
|
+
vi.stubGlobal(
|
|
229
|
+
'fetch',
|
|
230
|
+
vi.fn().mockResolvedValue({
|
|
231
|
+
ok: true,
|
|
232
|
+
status: 200,
|
|
233
|
+
json: async () => ({ visitor: { id: 'g-1', email: 'v@example.com', name: 'Visitor' } }),
|
|
234
|
+
}),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
const session = useSiteVisitorSession({ fetchOnMount: false })
|
|
238
|
+
await session.refresh()
|
|
239
|
+
|
|
240
|
+
expect(session.isAuthenticated.value).toBe(true)
|
|
241
|
+
expect(session.visitor.value?.email).toBe('v@example.com')
|
|
242
|
+
})
|
|
243
|
+
})
|
|
@@ -28,7 +28,12 @@
|
|
|
28
28
|
* The anonymous path — `200 {"visitor":null}`, a legacy `401`, a network error — stays
|
|
29
29
|
* exactly as silent as before.
|
|
30
30
|
*/
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
platformFetch,
|
|
33
|
+
readPlatformJson,
|
|
34
|
+
isPlatformFetchError,
|
|
35
|
+
type PlatformBodyClass,
|
|
36
|
+
} from '@duffcloudservices/cms-core'
|
|
32
37
|
import { ref, computed, onMounted, type Ref, type ComputedRef } from 'vue'
|
|
33
38
|
|
|
34
39
|
/** A signed-in site visitor. */
|
|
@@ -67,11 +72,23 @@ const SIGNED_OUT: SiteVisitorSessionResult = { visitor: null, authenticated: fal
|
|
|
67
72
|
const apiBase = (value?: string): string => (value ?? '/api/v1').replace(/\/$/u, '')
|
|
68
73
|
|
|
69
74
|
/**
|
|
70
|
-
* Normalize
|
|
71
|
-
*
|
|
72
|
-
*
|
|
75
|
+
* Normalize a parsed `/site-auth/session` BODY into a `SiteVisitor | null`.
|
|
76
|
+
*
|
|
77
|
+
* This is the payload-level answer to "is anyone signed in?" — the question the HTTP
|
|
78
|
+
* status cannot answer, because an anonymous visitor gets `200 { "visitor": null }`
|
|
79
|
+
* (A-35). Use it when your own code already parsed the body; use
|
|
80
|
+
* {@link readSiteVisitorSessionResponse} when you are holding a `Response`.
|
|
81
|
+
*
|
|
82
|
+
* `null` means signed out. Deliberately fail-closed on every ambiguity:
|
|
83
|
+
* - a MISSING `visitor` key yields `null`, never a truthy `undefined` — the trap that
|
|
84
|
+
* made kept's `visitor.value = data.visitor` a latent defect (C-619);
|
|
85
|
+
* - an envelope claiming `authenticated: true` with no visitor object yields `null`;
|
|
86
|
+
* - a visitor object without an email is not a visitor.
|
|
87
|
+
*
|
|
88
|
+
* Accepts the primary/legacy `{ visitor: {...} | null }` envelope and the contracts-spec
|
|
89
|
+
* `{ authenticated, user: {...} }` shape defensively.
|
|
73
90
|
*/
|
|
74
|
-
function
|
|
91
|
+
export function siteVisitorFromSessionPayload(data: unknown): SiteVisitor | null {
|
|
75
92
|
if (!data || typeof data !== 'object') {
|
|
76
93
|
return null
|
|
77
94
|
}
|
|
@@ -94,6 +111,81 @@ function extractVisitor(data: unknown): SiteVisitor | null {
|
|
|
94
111
|
}
|
|
95
112
|
}
|
|
96
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Body classes that mean the request never reached the platform API, as opposed to an
|
|
116
|
+
* API that answered. `empty` and `json` are NOT here: an empty 200 or a truncated body
|
|
117
|
+
* is an ordinary failed read, and reporting it as a misroute would cry wolf.
|
|
118
|
+
*/
|
|
119
|
+
const UNREACHABLE_BODY_CLASSES: ReadonlySet<PlatformBodyClass> = new Set<PlatformBodyClass>([
|
|
120
|
+
'spa-html',
|
|
121
|
+
'html',
|
|
122
|
+
'other',
|
|
123
|
+
])
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Decide "is a visitor signed in?" from a `/site-auth/session` `Response` you already
|
|
127
|
+
* fetched yourself.
|
|
128
|
+
*
|
|
129
|
+
* THIS IS THE SEAM FOR HAND-ROLLED SESSION STORES. {@link useSiteVisitorSession} owns its
|
|
130
|
+
* own fetch, which a site with an existing store (Pinia, caching, in-flight dedup, its own
|
|
131
|
+
* authenticated `apiFetch`) cannot adopt — so it hand-rolls the gate and re-derives the
|
|
132
|
+
* A-35 defect: `if (response.ok) return true`, which reports EVERY anonymous visitor as
|
|
133
|
+
* authenticated because anonymous is `200 { "visitor": null }`. Measured cost when that
|
|
134
|
+
* happened on kept: one member-gated 401 per page view, 346 in a day (C-619).
|
|
135
|
+
*
|
|
136
|
+
* Keep your fetch; hand the `Response` here and read the payload-derived answer:
|
|
137
|
+
*
|
|
138
|
+
* ```ts
|
|
139
|
+
* const response = await apiFetch('/api/v1/site-auth/session')
|
|
140
|
+
* const { visitor: v } = await readSiteVisitorSessionResponse(response)
|
|
141
|
+
* visitor.value = v
|
|
142
|
+
* return v !== null // never `return response.ok`
|
|
143
|
+
* ```
|
|
144
|
+
*
|
|
145
|
+
* Fails CLOSED everywhere: a non-2xx (incl. a legacy `401`), a missing response (your
|
|
146
|
+
* fetch threw), an unparseable body, or a `null` visitor all resolve to signed-out. The
|
|
147
|
+
* status can never on its own produce `authenticated: true` — only a real visitor object
|
|
148
|
+
* in the body can.
|
|
149
|
+
*
|
|
150
|
+
* Silent on every anonymous path. LOUD exactly once when the body was not JSON at all
|
|
151
|
+
* (`apiUnreachable: true`), because "this host does not route `/api/v1/*` to the API" must
|
|
152
|
+
* not masquerade as "signed out" — that ambiguity is how kept ran with nine dead login
|
|
153
|
+
* routes for ~2 weeks (C-261 / C-298 layer 2).
|
|
154
|
+
*
|
|
155
|
+
* Never throws.
|
|
156
|
+
*/
|
|
157
|
+
export async function readSiteVisitorSessionResponse(
|
|
158
|
+
response: Response | null | undefined,
|
|
159
|
+
): Promise<SiteVisitorSessionResult> {
|
|
160
|
+
// The caller's fetch rejected (network / abort / CORS) and passed us nothing.
|
|
161
|
+
if (!response) {
|
|
162
|
+
return SIGNED_OUT
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Old servers reply 401 to an anonymous probe; new servers reply 200-null. Treat any
|
|
166
|
+
// non-OK status as signed-out WITHOUT logging — and without reading the body, so a
|
|
167
|
+
// visitor object smuggled alongside a rejected status can never authenticate.
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
return SIGNED_OUT
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let data: unknown
|
|
173
|
+
try {
|
|
174
|
+
// `silent: true`: we decide what deserves the console below, so an ordinary failed
|
|
175
|
+
// read stays as quiet as it has always been.
|
|
176
|
+
data = await readPlatformJson<unknown>(response, { silent: true })
|
|
177
|
+
} catch (e) {
|
|
178
|
+
if (isPlatformFetchError(e) && UNREACHABLE_BODY_CLASSES.has(e.bodyClass)) {
|
|
179
|
+
console.error(e.message)
|
|
180
|
+
return { visitor: null, authenticated: false, apiUnreachable: true }
|
|
181
|
+
}
|
|
182
|
+
return SIGNED_OUT
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const visitor = siteVisitorFromSessionPayload(data)
|
|
186
|
+
return { visitor, authenticated: visitor !== null }
|
|
187
|
+
}
|
|
188
|
+
|
|
97
189
|
/**
|
|
98
190
|
* Fetch the current site-visitor session. Never throws and never logs for the
|
|
99
191
|
* anonymous case: any network error, non-OK status (incl. a legacy `401`), or
|
|
@@ -123,21 +215,9 @@ export async function fetchSiteVisitorSession(
|
|
|
123
215
|
return SIGNED_OUT
|
|
124
216
|
}
|
|
125
217
|
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
return SIGNED_OUT
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
let data: unknown
|
|
133
|
-
try {
|
|
134
|
-
data = await response.json()
|
|
135
|
-
} catch {
|
|
136
|
-
return SIGNED_OUT
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const visitor = extractVisitor(data)
|
|
140
|
-
return { visitor, authenticated: visitor !== null }
|
|
218
|
+
// ONE implementation of the gate: the composable, this function, and any site that
|
|
219
|
+
// adopts the seam directly all decide "authenticated" the same way, so they cannot drift.
|
|
220
|
+
return readSiteVisitorSessionResponse(response)
|
|
141
221
|
}
|
|
142
222
|
|
|
143
223
|
export interface UseSiteVisitorSessionOptions extends FetchSiteVisitorSessionOptions {
|