@exvio/os-backend-core 0.4.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 +466 -0
- package/package.json +48 -0
- package/src/ai/client.ts +1059 -0
- package/src/ai/errors.ts +114 -0
- package/src/ai/index.ts +27 -0
- package/src/ai/model-policy.ts +27 -0
- package/src/ai/pricing.ts +158 -0
- package/src/ai/providers/deepseek.ts +61 -0
- package/src/ai/providers/gemini.ts +919 -0
- package/src/ai/providers/openai-compatible.ts +731 -0
- package/src/ai/providers/sse.ts +163 -0
- package/src/ai/registry.ts +65 -0
- package/src/ai/schema.ts +382 -0
- package/src/ai/types.ts +282 -0
- package/src/auth/browser-exchange.ts +316 -0
- package/src/auth/errors.ts +57 -0
- package/src/auth/index.ts +29 -0
- package/src/auth/login-policy.ts +95 -0
- package/src/auth/oauth-state.ts +332 -0
- package/src/auth/passkey.ts +760 -0
- package/src/auth/redaction.ts +142 -0
- package/src/auth/session.ts +106 -0
- package/src/auth/types.ts +72 -0
- package/src/changelog/catalogue.ts +140 -0
- package/src/changelog/index.ts +5 -0
- package/src/changelog/locale.ts +122 -0
- package/src/changelog/service.ts +148 -0
- package/src/changelog/types.ts +122 -0
- package/src/db/bypass-tenant.ts +15 -0
- package/src/db/plugins/tenant-filter.ts +518 -0
- package/src/db/tenant-context.ts +51 -0
- package/src/guide/index.ts +5 -0
- package/src/guide/markdown.ts +108 -0
- package/src/guide/service.ts +345 -0
- package/src/guide/source.ts +116 -0
- package/src/guide/types.ts +177 -0
- package/src/index.ts +1 -0
- package/src/tenant.ts +14 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { AuthError } from './errors.ts'
|
|
2
|
+
import type {
|
|
3
|
+
LoginMode,
|
|
4
|
+
LoginPhase,
|
|
5
|
+
LoginPolicy,
|
|
6
|
+
LoginPolicyCheck,
|
|
7
|
+
LoginSurface,
|
|
8
|
+
} from './types.ts'
|
|
9
|
+
|
|
10
|
+
const LOGIN_MODES = new Set<LoginMode>(['hub_managed', 'standalone', 'hybrid'])
|
|
11
|
+
const LOGIN_SURFACES = new Set<LoginSurface>(['email', 'google', 'microsoft', 'passkey', 'hub_ott'])
|
|
12
|
+
const LOGIN_PHASES = new Set<LoginPhase>(['start', 'callback', 'finish'])
|
|
13
|
+
|
|
14
|
+
const MODE_DEFAULTS = Object.freeze({
|
|
15
|
+
hub_managed: Object.freeze(['hub_ott'] as const),
|
|
16
|
+
standalone: Object.freeze(['email', 'google', 'microsoft', 'passkey'] as const),
|
|
17
|
+
hybrid: Object.freeze(['email', 'google', 'microsoft', 'passkey', 'hub_ott'] as const),
|
|
18
|
+
}) satisfies Readonly<Record<LoginMode, readonly LoginSurface[]>>
|
|
19
|
+
|
|
20
|
+
export interface CreateLoginPolicyOptions {
|
|
21
|
+
readonly mode: LoginMode
|
|
22
|
+
/** May only narrow a mode's defaults. Omitted surfaces use the mode default. */
|
|
23
|
+
readonly enabled?: Readonly<Partial<Record<LoginSurface, boolean>>>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function invalidInput(): never {
|
|
27
|
+
throw new AuthError({ code: 'invalid_input', status: 400 })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function snapshotEnabled(value: unknown): Readonly<Partial<Record<LoginSurface, boolean>>> {
|
|
31
|
+
if (value === undefined) return Object.freeze({})
|
|
32
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) invalidInput()
|
|
33
|
+
|
|
34
|
+
let prototype: object | null
|
|
35
|
+
let descriptors: Record<string, PropertyDescriptor>
|
|
36
|
+
try {
|
|
37
|
+
prototype = Object.getPrototypeOf(value)
|
|
38
|
+
descriptors = Object.getOwnPropertyDescriptors(value)
|
|
39
|
+
} catch {
|
|
40
|
+
return invalidInput()
|
|
41
|
+
}
|
|
42
|
+
if (prototype !== Object.prototype && prototype !== null) invalidInput()
|
|
43
|
+
|
|
44
|
+
const snapshot: Partial<Record<LoginSurface, boolean>> = {}
|
|
45
|
+
for (const [key, descriptor] of Object.entries(descriptors)) {
|
|
46
|
+
if (!LOGIN_SURFACES.has(key as LoginSurface) || !('value' in descriptor)) invalidInput()
|
|
47
|
+
if (typeof descriptor.value !== 'boolean') invalidInput()
|
|
48
|
+
snapshot[key as LoginSurface] = descriptor.value
|
|
49
|
+
}
|
|
50
|
+
return Object.freeze(snapshot)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createLoginPolicy(options: CreateLoginPolicyOptions): LoginPolicy {
|
|
54
|
+
if (!options || typeof options !== 'object') invalidInput()
|
|
55
|
+
|
|
56
|
+
let mode: LoginMode
|
|
57
|
+
let configuredSurfaces: Readonly<Partial<Record<LoginSurface, boolean>>>
|
|
58
|
+
try {
|
|
59
|
+
mode = options.mode
|
|
60
|
+
configuredSurfaces = snapshotEnabled(options.enabled)
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error instanceof AuthError) throw error
|
|
63
|
+
return invalidInput()
|
|
64
|
+
}
|
|
65
|
+
if (!LOGIN_MODES.has(mode)) invalidInput()
|
|
66
|
+
|
|
67
|
+
const defaults = new Set<LoginSurface>(MODE_DEFAULTS[mode])
|
|
68
|
+
const enabled = new Set<LoginSurface>()
|
|
69
|
+
for (const surface of LOGIN_SURFACES) {
|
|
70
|
+
const configured = configuredSurfaces[surface]
|
|
71
|
+
// Configuration can fail closed by disabling a default. It cannot enable
|
|
72
|
+
// a credential surface forbidden by the selected mode.
|
|
73
|
+
if (defaults.has(surface) && configured !== false) enabled.add(surface)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const enabledSurfaces = Object.freeze([...enabled])
|
|
77
|
+
const allows = (check: LoginPolicyCheck): boolean => (
|
|
78
|
+
check !== null
|
|
79
|
+
&& typeof check === 'object'
|
|
80
|
+
&& LOGIN_SURFACES.has(check.surface)
|
|
81
|
+
&& LOGIN_PHASES.has(check.phase)
|
|
82
|
+
&& enabled.has(check.surface)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return Object.freeze({
|
|
86
|
+
mode,
|
|
87
|
+
enabledSurfaces,
|
|
88
|
+
allows,
|
|
89
|
+
assertAllowed(check: LoginPolicyCheck): void {
|
|
90
|
+
if (!allows(check)) {
|
|
91
|
+
throw new AuthError({ code: 'login_not_allowed', status: 403 })
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { AuthError } from './errors.ts'
|
|
2
|
+
import { createSecureAuthToken } from './session.ts'
|
|
3
|
+
import type {
|
|
4
|
+
OAuthStateConsumeInput,
|
|
5
|
+
OAuthStateIssueInput,
|
|
6
|
+
OAuthStateManager,
|
|
7
|
+
OAuthStatePayload,
|
|
8
|
+
OneTimeAuthStore,
|
|
9
|
+
} from './types.ts'
|
|
10
|
+
|
|
11
|
+
const OAUTH_STATE_PATTERN = /^oas_v1_[A-Za-z0-9_-]{43}$/
|
|
12
|
+
const PROVIDER_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/
|
|
13
|
+
const PKCE_VERIFIER_PATTERN = /^[A-Za-z0-9._~-]{43,128}$/
|
|
14
|
+
const SESSION_ID_PATTERN = /^[A-Za-z0-9._~-]{16,256}$/
|
|
15
|
+
const KEY_PREFIX_PATTERN = /^[A-Za-z0-9:_-]{1,64}$/
|
|
16
|
+
const encoder = new TextEncoder()
|
|
17
|
+
|
|
18
|
+
interface StoredOAuthState extends OAuthStatePayload {
|
|
19
|
+
readonly v: 1
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CreateOAuthStateManagerOptions {
|
|
23
|
+
readonly store: OneTimeAuthStore
|
|
24
|
+
readonly ttlSeconds?: number
|
|
25
|
+
readonly maxSerializedBytes?: number
|
|
26
|
+
readonly keyPrefix?: string
|
|
27
|
+
/** Millisecond clock, primarily for deterministic contract tests. */
|
|
28
|
+
readonly now?: () => number
|
|
29
|
+
/** Must return 32 bytes or more of unguessable base64url entropy in production. */
|
|
30
|
+
readonly generateState?: () => string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function invalidInput(): never {
|
|
34
|
+
throw new AuthError({ code: 'invalid_input', status: 400 })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function assertTenantId(tenantId: number): void {
|
|
38
|
+
if (!Number.isSafeInteger(tenantId) || tenantId <= 0) invalidInput()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function assertProvider(provider: string): void {
|
|
42
|
+
if (typeof provider !== 'string' || !PROVIDER_PATTERN.test(provider)) invalidInput()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function assertRedirectUri(redirectUri: string): void {
|
|
46
|
+
if (typeof redirectUri !== 'string' || redirectUri.length > 2_048) invalidInput()
|
|
47
|
+
let parsed: URL
|
|
48
|
+
try {
|
|
49
|
+
parsed = new URL(redirectUri)
|
|
50
|
+
} catch {
|
|
51
|
+
invalidInput()
|
|
52
|
+
}
|
|
53
|
+
if (
|
|
54
|
+
(parsed.protocol !== 'https:' && parsed.protocol !== 'http:')
|
|
55
|
+
|| parsed.username !== ''
|
|
56
|
+
|| parsed.password !== ''
|
|
57
|
+
|| parsed.hash !== ''
|
|
58
|
+
) invalidInput()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function assertVerifier(verifier: string): void {
|
|
62
|
+
if (typeof verifier !== 'string' || !PKCE_VERIFIER_PATTERN.test(verifier)) invalidInput()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function assertInitiatingSessionId(sessionId: string | undefined): void {
|
|
66
|
+
if (sessionId !== undefined && (
|
|
67
|
+
typeof sessionId !== 'string' || !SESSION_ID_PATTERN.test(sessionId)
|
|
68
|
+
)) invalidInput()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function assertReturnTo(returnTo: string | undefined): void {
|
|
72
|
+
if (returnTo === undefined) return
|
|
73
|
+
if (
|
|
74
|
+
typeof returnTo !== 'string'
|
|
75
|
+
|| returnTo.length === 0
|
|
76
|
+
|| returnTo.length > 2_048
|
|
77
|
+
|| !returnTo.startsWith('/')
|
|
78
|
+
|| returnTo.startsWith('//')
|
|
79
|
+
|| returnTo.includes('\\')
|
|
80
|
+
|| /[\u0000-\u001F\u007F]/u.test(returnTo)
|
|
81
|
+
) invalidInput()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function assertIssueInput(input: OAuthStateIssueInput): void {
|
|
85
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) invalidInput()
|
|
86
|
+
assertTenantId(input.tenantId)
|
|
87
|
+
assertProvider(input.provider)
|
|
88
|
+
assertRedirectUri(input.redirectUri)
|
|
89
|
+
assertVerifier(input.verifier)
|
|
90
|
+
assertInitiatingSessionId(input.initiatingSessionId)
|
|
91
|
+
assertReturnTo(input.returnTo)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function assertConsumeInput(input: OAuthStateConsumeInput): void {
|
|
95
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
96
|
+
throw new AuthError({ code: 'invalid_oauth_state', status: 400 })
|
|
97
|
+
}
|
|
98
|
+
assertTenantId(input.tenantId)
|
|
99
|
+
assertProvider(input.provider)
|
|
100
|
+
assertRedirectUri(input.redirectUri)
|
|
101
|
+
assertInitiatingSessionId(input.initiatingSessionId)
|
|
102
|
+
if (typeof input.state !== 'string' || !OAUTH_STATE_PATTERN.test(input.state)) {
|
|
103
|
+
throw new AuthError({ code: 'invalid_oauth_state', status: 400 })
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function base64Url(bytes: Uint8Array): string {
|
|
108
|
+
let binary = ''
|
|
109
|
+
for (const byte of bytes) binary += String.fromCharCode(byte)
|
|
110
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/u, '')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function hashState(state: string): Promise<string> {
|
|
114
|
+
const digest = await crypto.subtle.digest('SHA-256', encoder.encode(state))
|
|
115
|
+
return base64Url(new Uint8Array(digest))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function parseStoredPayload(serialized: string, maxSerializedBytes: number): StoredOAuthState {
|
|
119
|
+
if (encoder.encode(serialized).byteLength > maxSerializedBytes) {
|
|
120
|
+
throw new AuthError({ code: 'invalid_oauth_state', status: 400 })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let parsed: unknown
|
|
124
|
+
try {
|
|
125
|
+
parsed = JSON.parse(serialized)
|
|
126
|
+
} catch {
|
|
127
|
+
throw new AuthError({ code: 'invalid_oauth_state', status: 400 })
|
|
128
|
+
}
|
|
129
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
130
|
+
throw new AuthError({ code: 'invalid_oauth_state', status: 400 })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const value = parsed as Record<string, unknown>
|
|
134
|
+
const allowedKeys = new Set([
|
|
135
|
+
'v',
|
|
136
|
+
'tenantId',
|
|
137
|
+
'provider',
|
|
138
|
+
'redirectUri',
|
|
139
|
+
'verifier',
|
|
140
|
+
'initiatingSessionId',
|
|
141
|
+
'returnTo',
|
|
142
|
+
'issuedAt',
|
|
143
|
+
'expiresAt',
|
|
144
|
+
])
|
|
145
|
+
if (Object.keys(value).some(key => !allowedKeys.has(key))) {
|
|
146
|
+
throw new AuthError({ code: 'invalid_oauth_state', status: 400 })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
if (value.v !== 1) invalidInput()
|
|
151
|
+
assertTenantId(value.tenantId as number)
|
|
152
|
+
assertProvider(value.provider as string)
|
|
153
|
+
assertRedirectUri(value.redirectUri as string)
|
|
154
|
+
assertVerifier(value.verifier as string)
|
|
155
|
+
assertInitiatingSessionId(value.initiatingSessionId as string | undefined)
|
|
156
|
+
assertReturnTo(value.returnTo as string | undefined)
|
|
157
|
+
if (
|
|
158
|
+
typeof value.issuedAt !== 'number'
|
|
159
|
+
|| !Number.isSafeInteger(value.issuedAt)
|
|
160
|
+
|| value.issuedAt < 0
|
|
161
|
+
|| typeof value.expiresAt !== 'number'
|
|
162
|
+
|| !Number.isSafeInteger(value.expiresAt)
|
|
163
|
+
|| value.expiresAt <= value.issuedAt
|
|
164
|
+
|| value.expiresAt - value.issuedAt > 3_600_000
|
|
165
|
+
) invalidInput()
|
|
166
|
+
} catch {
|
|
167
|
+
throw new AuthError({ code: 'invalid_oauth_state', status: 400 })
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return value as unknown as StoredOAuthState
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Issue and atomically consume a tenant-bound OAuth transaction.
|
|
175
|
+
* The backing key contains only SHA-256(state), so Redis/profiler key logs do
|
|
176
|
+
* not disclose a replayable raw state token.
|
|
177
|
+
*/
|
|
178
|
+
export function createOAuthStateManager(
|
|
179
|
+
options: CreateOAuthStateManagerOptions,
|
|
180
|
+
): OAuthStateManager {
|
|
181
|
+
if (
|
|
182
|
+
!options
|
|
183
|
+
|| typeof options !== 'object'
|
|
184
|
+
|| !options.store
|
|
185
|
+
|| typeof options.store.put !== 'function'
|
|
186
|
+
|| typeof options.store.take !== 'function'
|
|
187
|
+
) {
|
|
188
|
+
invalidInput()
|
|
189
|
+
}
|
|
190
|
+
const ttlSeconds = options.ttlSeconds ?? 600
|
|
191
|
+
const maxSerializedBytes = options.maxSerializedBytes ?? 4_096
|
|
192
|
+
const keyPrefix = options.keyPrefix ?? 'auth:oauth-state:v1:'
|
|
193
|
+
const now = options.now ?? Date.now
|
|
194
|
+
const generateState = options.generateState ?? (() => `oas_v1_${createSecureAuthToken()}`)
|
|
195
|
+
|
|
196
|
+
if (!Number.isSafeInteger(ttlSeconds) || ttlSeconds < 30 || ttlSeconds > 3_600) invalidInput()
|
|
197
|
+
if (
|
|
198
|
+
!Number.isSafeInteger(maxSerializedBytes)
|
|
199
|
+
|| maxSerializedBytes < 512
|
|
200
|
+
|| maxSerializedBytes > 16_384
|
|
201
|
+
|| !KEY_PREFIX_PATTERN.test(keyPrefix)
|
|
202
|
+
|| typeof now !== 'function'
|
|
203
|
+
|| typeof generateState !== 'function'
|
|
204
|
+
) invalidInput()
|
|
205
|
+
|
|
206
|
+
// Capture and bind the adapter methods themselves. Reassigning `store.put`
|
|
207
|
+
// or `store.take` after construction cannot downgrade atomic semantics.
|
|
208
|
+
const put = options.store.put.bind(options.store)
|
|
209
|
+
const take = options.store.take.bind(options.store)
|
|
210
|
+
|
|
211
|
+
// Copy every option once. Mutating the caller-owned options object cannot
|
|
212
|
+
// relax policy or change transaction semantics after construction.
|
|
213
|
+
const snapshot = Object.freeze({
|
|
214
|
+
put,
|
|
215
|
+
take,
|
|
216
|
+
ttlSeconds,
|
|
217
|
+
maxSerializedBytes,
|
|
218
|
+
keyPrefix,
|
|
219
|
+
now,
|
|
220
|
+
generateState,
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
const storageKey = async (state: string): Promise<string> => (
|
|
224
|
+
`${snapshot.keyPrefix}${await hashState(state)}`
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
return Object.freeze({
|
|
228
|
+
async issue(input: OAuthStateIssueInput): Promise<string> {
|
|
229
|
+
assertIssueInput(input)
|
|
230
|
+
let state: string
|
|
231
|
+
let issuedAt: number
|
|
232
|
+
try {
|
|
233
|
+
state = snapshot.generateState()
|
|
234
|
+
issuedAt = snapshot.now()
|
|
235
|
+
} catch (error) {
|
|
236
|
+
throw new AuthError({ code: 'invalid_oauth_state', status: 500, cause: error })
|
|
237
|
+
}
|
|
238
|
+
if (typeof state !== 'string' || !OAUTH_STATE_PATTERN.test(state)) {
|
|
239
|
+
throw new AuthError({ code: 'invalid_oauth_state', status: 500 })
|
|
240
|
+
}
|
|
241
|
+
if (!Number.isSafeInteger(issuedAt) || issuedAt < 0) invalidInput()
|
|
242
|
+
const expiresAt = issuedAt + (snapshot.ttlSeconds * 1_000)
|
|
243
|
+
if (!Number.isSafeInteger(expiresAt)) invalidInput()
|
|
244
|
+
|
|
245
|
+
const payload: StoredOAuthState = {
|
|
246
|
+
v: 1,
|
|
247
|
+
tenantId: input.tenantId,
|
|
248
|
+
provider: input.provider,
|
|
249
|
+
redirectUri: input.redirectUri,
|
|
250
|
+
verifier: input.verifier,
|
|
251
|
+
...(input.initiatingSessionId === undefined
|
|
252
|
+
? {}
|
|
253
|
+
: { initiatingSessionId: input.initiatingSessionId }),
|
|
254
|
+
...(input.returnTo === undefined ? {} : { returnTo: input.returnTo }),
|
|
255
|
+
issuedAt,
|
|
256
|
+
expiresAt,
|
|
257
|
+
}
|
|
258
|
+
const serialized = JSON.stringify(payload)
|
|
259
|
+
if (encoder.encode(serialized).byteLength > snapshot.maxSerializedBytes) invalidInput()
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
await snapshot.put(
|
|
263
|
+
await storageKey(state),
|
|
264
|
+
serialized,
|
|
265
|
+
snapshot.ttlSeconds,
|
|
266
|
+
)
|
|
267
|
+
} catch (error) {
|
|
268
|
+
throw new AuthError({
|
|
269
|
+
code: 'auth_store_unavailable',
|
|
270
|
+
status: 503,
|
|
271
|
+
retryable: true,
|
|
272
|
+
cause: error,
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
return state
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
async consume(input: OAuthStateConsumeInput): Promise<OAuthStatePayload> {
|
|
279
|
+
assertConsumeInput(input)
|
|
280
|
+
let serialized: string | null
|
|
281
|
+
try {
|
|
282
|
+
serialized = await snapshot.take(await storageKey(input.state))
|
|
283
|
+
} catch (error) {
|
|
284
|
+
throw new AuthError({
|
|
285
|
+
code: 'auth_store_unavailable',
|
|
286
|
+
status: 503,
|
|
287
|
+
retryable: true,
|
|
288
|
+
cause: error,
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
if (serialized === null) {
|
|
292
|
+
throw new AuthError({ code: 'invalid_oauth_state', status: 400 })
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const payload = parseStoredPayload(serialized, snapshot.maxSerializedBytes)
|
|
296
|
+
let currentTime: number
|
|
297
|
+
try {
|
|
298
|
+
currentTime = snapshot.now()
|
|
299
|
+
} catch (error) {
|
|
300
|
+
throw new AuthError({ code: 'invalid_oauth_state', status: 500, cause: error })
|
|
301
|
+
}
|
|
302
|
+
if (!Number.isSafeInteger(currentTime) || currentTime < 0) invalidInput()
|
|
303
|
+
if (payload.issuedAt > currentTime) {
|
|
304
|
+
throw new AuthError({ code: 'invalid_oauth_state', status: 400 })
|
|
305
|
+
}
|
|
306
|
+
if (payload.expiresAt <= currentTime) {
|
|
307
|
+
throw new AuthError({ code: 'oauth_state_expired', status: 400 })
|
|
308
|
+
}
|
|
309
|
+
if (
|
|
310
|
+
payload.tenantId !== input.tenantId
|
|
311
|
+
|| payload.provider !== input.provider
|
|
312
|
+
|| payload.redirectUri !== input.redirectUri
|
|
313
|
+
|| payload.initiatingSessionId !== input.initiatingSessionId
|
|
314
|
+
) {
|
|
315
|
+
throw new AuthError({ code: 'oauth_state_mismatch', status: 400 })
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return Object.freeze({
|
|
319
|
+
tenantId: payload.tenantId,
|
|
320
|
+
provider: payload.provider,
|
|
321
|
+
redirectUri: payload.redirectUri,
|
|
322
|
+
verifier: payload.verifier,
|
|
323
|
+
...(payload.initiatingSessionId === undefined
|
|
324
|
+
? {}
|
|
325
|
+
: { initiatingSessionId: payload.initiatingSessionId }),
|
|
326
|
+
...(payload.returnTo === undefined ? {} : { returnTo: payload.returnTo }),
|
|
327
|
+
issuedAt: payload.issuedAt,
|
|
328
|
+
expiresAt: payload.expiresAt,
|
|
329
|
+
})
|
|
330
|
+
},
|
|
331
|
+
})
|
|
332
|
+
}
|