okengine 0.17.2 → 0.18.3
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 +9 -5
- package/manifest.v1.schema.json +61 -2
- package/package.json +18 -4
- package/site/content/docs/elements/clock.mdx +1 -1
- package/site/content/docs/elements/flow.mdx +25 -1
- package/site/content/docs/elements/store.mdx +287 -341
- package/site/content/docs/elements/vault.mdx +5 -5
- package/site/content/docs/get-started/installation.mdx +1 -2
- package/site/content/docs/get-started/introduction.mdx +54 -110
- package/site/content/docs/get-started/meta.json +9 -1
- package/site/content/docs/get-started/testing.mdx +328 -0
- package/site/content/docs/get-started/why.mdx +94 -70
- package/site/content/docs/index.mdx +1 -1
- package/site/content/docs/plugins/apple.mdx +151 -0
- package/site/content/docs/plugins/discord.mdx +139 -0
- package/site/content/docs/plugins/facebook.mdx +134 -0
- package/site/content/docs/plugins/figma.mdx +138 -0
- package/site/content/docs/plugins/github.mdx +138 -0
- package/site/content/docs/plugins/google.mdx +153 -0
- package/site/content/docs/plugins/index.mdx +47 -1
- package/site/content/docs/plugins/meta.json +10 -0
- package/site/content/docs/plugins/microsoft.mdx +151 -0
- package/site/content/docs/plugins/oauth.mdx +188 -0
- package/site/content/docs/plugins/x.mdx +125 -0
- package/site/content/docs/reference/cli.md +3 -2
- package/site/content/docs/reference/client.mdx +58 -1
- package/site/content/docs/reference/configuration.mdx +2 -4
- package/site/content/docs/reference/fx.mdx +3 -1
- package/site/content/docs/reference/index.mdx +0 -5
- package/site/content/docs/reference/meta.json +2 -2
- package/site/content/docs/reference/okid.mdx +137 -0
- package/src/auth/bindings.ts +1 -1
- package/src/auth/config.ts +9 -0
- package/src/auth/identity-sql.ts +314 -0
- package/src/auth/identity.ts +140 -2
- package/src/auth/index.ts +17 -1
- package/src/auth/method-context.ts +3 -0
- package/src/auth/oauth-as/cimd.ts +132 -0
- package/src/auth/oauth-as/crypto.test.ts +101 -0
- package/src/auth/oauth-as/crypto.ts +393 -0
- package/src/auth/oauth-as/errors.ts +68 -0
- package/src/auth/oauth-as/http.test.ts +419 -0
- package/src/auth/oauth-as/http.ts +842 -0
- package/src/auth/oauth-as/stores.ts +61 -0
- package/src/auth/oauth-as/tables.ts +142 -0
- package/src/auth/tables.ts +0 -11
- package/src/bench/README.md +83 -0
- package/src/bench/REPORT.md +176 -0
- package/src/bench/g01-rls-stamp.bench.ts +194 -0
- package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
- package/src/bench/g03-signal-once.bench.ts +157 -0
- package/src/bench/g03-signal-reconnect.bench.ts +254 -0
- package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
- package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
- package/src/bench/g05-sustained-full.bench.ts +265 -0
- package/src/bench/g06-mixed-load.bench.ts +260 -0
- package/src/bench/g07-vault-crypto.bench.ts +100 -0
- package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
- package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
- package/src/bench/g08-store-kv-durable.bench.ts +133 -0
- package/src/bench/g08-store-sql.bench.ts +178 -0
- package/src/bench/g09-journal-sustained.bench.ts +203 -0
- package/src/bench/g10-observability-contention.bench.ts +246 -0
- package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
- package/src/bench/g13-elements.bench.ts +427 -0
- package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
- package/src/bench/g15-postgres-degradation.bench.ts +264 -0
- package/src/bench/g16-live-query-fanout.bench.ts +206 -0
- package/src/bench/lib/event-loop-lag.ts +26 -0
- package/src/bench/lib/infra.ts +60 -0
- package/src/bench/lib/report.ts +52 -0
- package/src/bench/lib/rss-sampler.ts +61 -0
- package/src/bench/lib/signal-pg.ts +88 -0
- package/src/bench/load-app.ts +337 -0
- package/src/bench/load-child.ts +108 -0
- package/src/bench/smoke.bench.ts +43 -0
- package/src/cli/competitor-mention-removal.test.ts +28 -0
- package/src/cli/doctor-fd.ts +117 -0
- package/src/cli/doctor.test.ts +192 -0
- package/src/cli/doctor.ts +129 -1
- package/src/client/create.ts +95 -1
- package/src/client/index.ts +9 -2
- package/src/client/transport.ts +11 -4
- package/src/client/use-live-query.ts +154 -0
- package/src/client-react/index.ts +15 -1
- package/src/client-react/live-resource.ts +246 -0
- package/src/client-react/use-live-query.test.ts +475 -0
- package/src/client-react/use-live-query.ts +530 -0
- package/src/compiler/extract.test.ts +518 -0
- package/src/compiler/extract.ts +386 -19
- package/src/console/server/invoke-user-flow.ts +2 -1
- package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-DY4N6nnk.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-CsPDMrVM.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-CcTDXHuz.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-CKR595wP.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
- package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-CpPFFKyE.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-BsMf-9_W.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
- package/src/drivers/cdc-outbox.ts +389 -0
- package/src/drivers/memory.ts +20 -0
- package/src/drivers/oauth-apple.ts +156 -0
- package/src/drivers/oauth-discord.ts +79 -0
- package/src/drivers/oauth-facebook.ts +80 -0
- package/src/drivers/oauth-figma.ts +116 -0
- package/src/drivers/oauth-github.ts +92 -0
- package/src/drivers/oauth-google.ts +142 -0
- package/src/drivers/oauth-microsoft.ts +174 -0
- package/src/drivers/oauth-oidc.ts +293 -0
- package/src/drivers/oauth-shared.ts +326 -0
- package/src/drivers/oauth-types.ts +159 -0
- package/src/drivers/oauth-x.ts +77 -0
- package/src/drivers/oauth2-common.ts +95 -0
- package/src/drivers/oauth2-token.ts +61 -0
- package/src/drivers/pg-rls-row-passes.ts +251 -0
- package/src/drivers/pg-rls.ts +2 -0
- package/src/drivers/postgres.ts +45 -2
- package/src/drivers/signal-postgres.ts +2 -1
- package/src/elements/channel/runtime.ts +29 -2
- package/src/elements/channel.test.ts +52 -0
- package/src/elements/gate/boot.ts +29 -2
- package/src/elements/store/emit-drizzle.ts +147 -14
- package/src/elements/store/field-ddl.test.ts +118 -0
- package/src/elements/store/field-types.test.ts +455 -0
- package/src/elements/store/list-query.golden.json +777 -0
- package/src/elements/store/list-query.parity.test.ts +396 -0
- package/src/elements/store/list-query.ts +792 -0
- package/src/elements/store/live-default.test.ts +136 -0
- package/src/elements/store/live-http.test.ts +160 -0
- package/src/elements/store/live-isolation.test.ts +291 -0
- package/src/elements/store/live-query-runtime.test.ts +323 -0
- package/src/elements/store/live-query-runtime.ts +403 -0
- package/src/elements/store/live-query-server.test.ts +377 -0
- package/src/elements/store/live-query-server.ts +102 -0
- package/src/elements/store/live-query.ts +97 -0
- package/src/elements/store/resource.ts +189 -680
- package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
- package/src/elements/store/schema-decl.ts +539 -41
- package/src/elements/store/sql-rls-stamp.test.ts +27 -0
- package/src/elements/store/sql-session.ts +297 -35
- package/src/elements/store/table.ts +102 -21
- package/src/elements/store.test.ts +3 -1
- package/src/elements/store.ts +12 -1
- package/src/elements/vault/chaos-child.ts +74 -1
- package/src/elements/vault/chaos.test.ts +4 -2
- package/src/elements/vault/storage.ts +4 -2
- package/src/index.ts +4 -1
- package/src/kernel/app-auth.ts +1 -0
- package/src/kernel/app.ts +116 -2
- package/src/kernel/auth-sharing.test.ts +196 -0
- package/src/kernel/boot.test.ts +3 -3
- package/src/kernel/errors.ts +8 -0
- package/src/kernel/fx.test.ts +1 -0
- package/src/kernel/fx.ts +14 -2
- package/src/kernel/horizontal-child.ts +2 -1
- package/src/kernel/http-resource.ts +33 -7
- package/src/kernel/identity-host-persist.test.ts +119 -0
- package/src/kernel/instance-id.ts +4 -2
- package/src/kernel/journal.ts +2 -1
- package/src/kernel/mcp-tool.test.ts +95 -0
- package/src/kernel/on.ts +9 -0
- package/src/kernel/realtime-bind.ts +326 -0
- package/src/kernel/resource-live.ts +117 -0
- package/src/kernel/triggers.ts +86 -4
- package/src/manifest/diff.ts +37 -0
- package/src/manifest/types.ts +64 -2
- package/src/okid.bench.test.ts +64 -0
- package/src/okid.test.ts +338 -0
- package/src/okid.ts +245 -0
- package/src/plugins/anonymous.ts +19 -1
- package/src/plugins/auth/shared.ts +15 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/magic-link.ts +10 -8
- package/src/plugins/mcp-oauth.ts +208 -0
- package/src/plugins/oauth/flow-store.ts +117 -0
- package/src/plugins/oauth/link.ts +69 -0
- package/src/plugins/oauth/shared.ts +108 -0
- package/src/plugins/oauth/token-vault.ts +100 -0
- package/src/plugins/oauth.security.test.ts +535 -0
- package/src/plugins/oauth.ts +532 -0
- package/src/plugins/otp.ts +48 -6
- package/src/plugins/passkey.ts +20 -1
- package/src/plugins/two-factor.ts +11 -0
- package/src/plugins/username.ts +40 -7
- package/src/release/build-lib.ts +7 -1
- package/src/release/measure.ts +1 -0
- package/src/release/official-plugins.ts +4 -1
- package/src/runs/collect.ts +2 -1
- package/src/runs/drivers/files.ts +2 -1
- package/src/test/create-test-app.ts +114 -5
- package/src/test/export-bundle.test.ts +33 -0
- package/src/test/live-signals.test.ts +83 -0
- package/src/test/tenant-isolation.test.ts +175 -0
- package/src/testing.ts +26 -0
- package/src/upgrade/codemods.ts +1 -1
- package/site/content/docs/reference/migrating-environments.mdx +0 -158
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth social-login plugin — Authorization Code + PKCE for eight providers,
|
|
3
|
+
* routed through `linkOrProvision` with the per-provider email trust matrix.
|
|
4
|
+
*
|
|
5
|
+
* Security posture (locked decisions):
|
|
6
|
+
* - PKCE S256 mandatory; implicit / ROPC never implemented
|
|
7
|
+
* - Single-use `state` rows (SHA-256-hashed at rest) on the verification store
|
|
8
|
+
* - Mix-up defense (RFC 9700 §4.4 / RFC 9207): flow records pin the initiated
|
|
9
|
+
* issuer; callbacks verify route provider == stored provider and, for OIDC,
|
|
10
|
+
* ID token `iss` == stored expectation (Microsoft `{tenantid}` template via
|
|
11
|
+
* concrete per-tenant regex)
|
|
12
|
+
* - Exact registered redirect URIs — never derived from the request
|
|
13
|
+
* - Unverified emails cannot take over existing accounts (`linkOrProvision`
|
|
14
|
+
* refuses `email_in_use` without an authenticated `currentUserId`)
|
|
15
|
+
* - Provider tokens are NOT stored unless `storeProviderTokens`; then only in
|
|
16
|
+
* Vault behind envelope sealing
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import {
|
|
21
|
+
authPublicGates,
|
|
22
|
+
bindPublicAuth,
|
|
23
|
+
bindSessionAuth,
|
|
24
|
+
createMethodRuntime,
|
|
25
|
+
fail,
|
|
26
|
+
flow,
|
|
27
|
+
http,
|
|
28
|
+
resolveSharedIdentities,
|
|
29
|
+
} from "./auth/shared.ts";
|
|
30
|
+
import { bindAuthHttp } from "../auth/bindings.ts";
|
|
31
|
+
import { hashChallenge } from "../auth/verification.ts";
|
|
32
|
+
import { issueSessionWithScopes } from "../auth/sessions.ts";
|
|
33
|
+
import type { Binding } from "../kernel/on.ts";
|
|
34
|
+
import type { Fx } from "../kernel/fx.ts";
|
|
35
|
+
import type { AnyFlowDef } from "../kernel/flow.ts";
|
|
36
|
+
import { identityFailureReason, oauthLink } from "./oauth/link.ts";
|
|
37
|
+
import {
|
|
38
|
+
consumeOauthFlow,
|
|
39
|
+
createOAuthFlowStore,
|
|
40
|
+
findOauthFlow,
|
|
41
|
+
putOauthFlow,
|
|
42
|
+
} from "./oauth/flow-store.ts";
|
|
43
|
+
import {
|
|
44
|
+
mintPkcePair,
|
|
45
|
+
OAUTH_PROVIDER_IDS,
|
|
46
|
+
bindPublicAuthGet,
|
|
47
|
+
type OAuthOptions,
|
|
48
|
+
type OAuthProviderConfig,
|
|
49
|
+
type OAuthProviderId,
|
|
50
|
+
} from "./oauth/shared.ts";
|
|
51
|
+
import { sealTokenEnvelope, unsealTokenEnvelope } from "./oauth/token-vault.ts";
|
|
52
|
+
import { generateState } from "../drivers/oauth-shared.ts";
|
|
53
|
+
import type { OAuthDriver, OAuthTokenSet } from "../drivers/oauth-types.ts";
|
|
54
|
+
import { openOAuthGoogle } from "../drivers/oauth-google.ts";
|
|
55
|
+
import { openOAuthApple } from "../drivers/oauth-apple.ts";
|
|
56
|
+
import { openOAuthMicrosoft } from "../drivers/oauth-microsoft.ts";
|
|
57
|
+
import { openOAuthGithub } from "../drivers/oauth-github.ts";
|
|
58
|
+
import { openOAuthDiscord } from "../drivers/oauth-discord.ts";
|
|
59
|
+
import { openOAuthX } from "../drivers/oauth-x.ts";
|
|
60
|
+
import { openOAuthFacebook } from "../drivers/oauth-facebook.ts";
|
|
61
|
+
import { openOAuthFigma } from "../drivers/oauth-figma.ts";
|
|
62
|
+
import { createAppleClientSecretJwt } from "../drivers/oauth-oidc.ts";
|
|
63
|
+
import { vault } from "../elements/vault.ts";
|
|
64
|
+
import { plugin } from "../kernel/plugin.ts";
|
|
65
|
+
|
|
66
|
+
export { OAUTH_PROVIDER_IDS };
|
|
67
|
+
export type { OAuthOptions, OAuthProviderConfig, OAuthProviderId };
|
|
68
|
+
|
|
69
|
+
const PROVIDER_ENUM = z.enum(OAUTH_PROVIDER_IDS);
|
|
70
|
+
|
|
71
|
+
/** Output of the start / link-start Flows. */
|
|
72
|
+
const StartOut = z.object({
|
|
73
|
+
provider: PROVIDER_ENUM,
|
|
74
|
+
authorizationUrl: z.string(),
|
|
75
|
+
expiresInMs: z.number(),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/** Session payload issued on a successful callback. */
|
|
79
|
+
const SessionTokensOut = z.object({
|
|
80
|
+
accessToken: z.string(),
|
|
81
|
+
refreshToken: z.string(),
|
|
82
|
+
accessExpiresAt: z.number(),
|
|
83
|
+
userId: z.string(),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const AuthFailed = z.object({ reason: z.string().optional() });
|
|
87
|
+
const AuthRateLimited = z.object({ reason: z.string() });
|
|
88
|
+
|
|
89
|
+
const StartIn = z.object({ provider: PROVIDER_ENUM });
|
|
90
|
+
|
|
91
|
+
const CallbackIn = z.object({
|
|
92
|
+
provider: PROVIDER_ENUM,
|
|
93
|
+
code: z.string().optional(),
|
|
94
|
+
state: z.string().optional(),
|
|
95
|
+
error: z.string().optional(),
|
|
96
|
+
/** Apple `response_mode=form_post` delivers urlencoded bodies here. */
|
|
97
|
+
body: z.unknown().optional(),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/** Resolved per-provider runtime bundle. */
|
|
101
|
+
interface ProviderRuntime {
|
|
102
|
+
readonly id: OAuthProviderId;
|
|
103
|
+
readonly driver: OAuthDriver;
|
|
104
|
+
readonly config: OAuthProviderConfig;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Social login plugin. Enable `gate.auth`, configure providers, then
|
|
109
|
+
* `.plug(oauth({...}))`.
|
|
110
|
+
*
|
|
111
|
+
* @param options - Provider configs, base URL, optional shared stores/fetch
|
|
112
|
+
*/
|
|
113
|
+
export function oauth(options: OAuthOptions = {}) {
|
|
114
|
+
const runtime = createMethodRuntime(options);
|
|
115
|
+
const identities = resolveSharedIdentities(options);
|
|
116
|
+
const verifications = createOAuthFlowStore(options.verifications);
|
|
117
|
+
const now: () => number = () => runtime.now();
|
|
118
|
+
const providers = buildProviders(options);
|
|
119
|
+
|
|
120
|
+
const start = flow("auth.oauthStart", {
|
|
121
|
+
plane: "user",
|
|
122
|
+
in: StartIn,
|
|
123
|
+
out: StartOut,
|
|
124
|
+
errors: { AuthFailed, AuthRateLimited },
|
|
125
|
+
effects: {},
|
|
126
|
+
do: async (input) => {
|
|
127
|
+
const p = providers.find((c) => c.id === input.provider);
|
|
128
|
+
if (!p) return fail("AuthFailed", { reason: "provider_disabled" });
|
|
129
|
+
const redirectUri = resolveRedirectUri(p, options);
|
|
130
|
+
const clientId = p.config.clientId;
|
|
131
|
+
if (!redirectUri || !clientId) {
|
|
132
|
+
return fail("AuthFailed", { reason: "provider_misconfigured" });
|
|
133
|
+
}
|
|
134
|
+
const state = generateState();
|
|
135
|
+
const { verifier, challenge } = await mintPkcePair();
|
|
136
|
+
const nonce = p.driver.kind === "oidc" ? generateState() : undefined;
|
|
137
|
+
await putOauthFlow(
|
|
138
|
+
verifications,
|
|
139
|
+
{
|
|
140
|
+
provider: p.id,
|
|
141
|
+
stateHash: await hashChallenge(state),
|
|
142
|
+
codeVerifier: verifier,
|
|
143
|
+
redirectUri,
|
|
144
|
+
...(nonce !== undefined ? { nonce } : {}),
|
|
145
|
+
expectedIssuer: p.driver.authorizationServerId,
|
|
146
|
+
clientId,
|
|
147
|
+
},
|
|
148
|
+
now(),
|
|
149
|
+
);
|
|
150
|
+
return {
|
|
151
|
+
provider: p.id,
|
|
152
|
+
authorizationUrl: p.driver.buildAuthorizeUrl({
|
|
153
|
+
clientId,
|
|
154
|
+
redirectUri,
|
|
155
|
+
scopes: p.config.scopes ?? [],
|
|
156
|
+
state,
|
|
157
|
+
codeChallenge: challenge,
|
|
158
|
+
...(nonce !== undefined ? { nonce } : {}),
|
|
159
|
+
}).url,
|
|
160
|
+
expiresInMs: OAUTH_FLOW_TTL_MS,
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const callback = flow("auth.oauthCallback", {
|
|
166
|
+
plane: "user",
|
|
167
|
+
in: CallbackIn,
|
|
168
|
+
out: SessionTokensOut,
|
|
169
|
+
errors: { AuthFailed, AuthRateLimited },
|
|
170
|
+
effects: { secrets: callbackSecretRefs(providers) },
|
|
171
|
+
do: async (input, fx) => {
|
|
172
|
+
if (typeof input.error === "string" && input.error.length > 0) {
|
|
173
|
+
return fail("AuthFailed", { reason: "authorization_failed" });
|
|
174
|
+
}
|
|
175
|
+
const code = input.code ?? fieldFromBody(input.body, "code");
|
|
176
|
+
const state = input.state ?? fieldFromBody(input.body, "state");
|
|
177
|
+
if (!code || !state) return fail("AuthFailed", { reason: "invalid_callback" });
|
|
178
|
+
const t = now();
|
|
179
|
+
|
|
180
|
+
// Mix-up defense layer 1 — rows are keyed `oauth:{provider}:{hash}`,
|
|
181
|
+
// so a state minted for another provider never matches this route.
|
|
182
|
+
const record = await findOauthFlow(verifications, input.provider, state, t);
|
|
183
|
+
if (!record || record.provider !== input.provider) {
|
|
184
|
+
return fail("AuthFailed", { reason: "invalid_state" });
|
|
185
|
+
}
|
|
186
|
+
await consumeOauthFlow(verifications, input.provider, state, t);
|
|
187
|
+
|
|
188
|
+
const p = providers.find((c) => c.id === record.provider);
|
|
189
|
+
if (!p) return fail("AuthFailed", { reason: "invalid_state" });
|
|
190
|
+
|
|
191
|
+
let credentials: Awaited<ReturnType<typeof loadCredentials>>;
|
|
192
|
+
try {
|
|
193
|
+
credentials = await loadCredentials(p, fx);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
return fail("AuthFailed", { reason: credentialFailureReason(err) });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Protocol phase — exchange + assertion. Any failure here is the
|
|
199
|
+
// provider's or an attacker's; nothing has touched identity state.
|
|
200
|
+
let assertion;
|
|
201
|
+
let exchangedTokens: OAuthTokenSet | undefined;
|
|
202
|
+
try {
|
|
203
|
+
const exchanged = await p.driver.exchangeCode({
|
|
204
|
+
clientId: record.clientId,
|
|
205
|
+
redirectUri: record.redirectUri,
|
|
206
|
+
code,
|
|
207
|
+
codeVerifier: record.codeVerifier,
|
|
208
|
+
...(options.fetch !== undefined ? { fetch: options.fetch } : {}),
|
|
209
|
+
...credentials,
|
|
210
|
+
});
|
|
211
|
+
exchangedTokens = exchanged.tokens;
|
|
212
|
+
// Mix-up defense layer 2 — drivers verify ID-token `iss` against the
|
|
213
|
+
// flow-record expectation; OAuth2 assertions carry the pinned AS id.
|
|
214
|
+
assertion = await p.driver.resolveAssertion({
|
|
215
|
+
tokens: exchanged.tokens,
|
|
216
|
+
expectedIssuer: record.expectedIssuer,
|
|
217
|
+
expectedAudience: record.clientId,
|
|
218
|
+
...(record.nonce !== undefined ? { expectedNonce: record.nonce } : {}),
|
|
219
|
+
});
|
|
220
|
+
} catch (err) {
|
|
221
|
+
return fail("AuthFailed", { reason: protocolReason(err) });
|
|
222
|
+
}
|
|
223
|
+
if (assertion.issuer !== record.expectedIssuer) {
|
|
224
|
+
return fail("AuthFailed", { reason: "issuer_mismatch" });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Identity phase — the ONLY path identities are ever written from.
|
|
228
|
+
let linked;
|
|
229
|
+
try {
|
|
230
|
+
linked = await oauthLink(identities, p.id, assertion, record.currentUserId);
|
|
231
|
+
} catch (err) {
|
|
232
|
+
const reason = identityFailureReason(err);
|
|
233
|
+
if (reason === undefined) throw err;
|
|
234
|
+
return fail("AuthFailed", { reason });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (p.config.storeProviderTokens === true && exchangedTokens !== undefined) {
|
|
238
|
+
await persistProviderTokens(
|
|
239
|
+
p.id,
|
|
240
|
+
linked.userId,
|
|
241
|
+
exchangedTokens,
|
|
242
|
+
runtime.crypto.secret,
|
|
243
|
+
fx,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
const issued = await issueSessionWithScopes(runtime.sessions, runtime.crypto, {
|
|
247
|
+
id: linked.userId,
|
|
248
|
+
plane: "user",
|
|
249
|
+
scopes: [],
|
|
250
|
+
});
|
|
251
|
+
return {
|
|
252
|
+
accessToken: issued.accessToken,
|
|
253
|
+
refreshToken: issued.refreshToken,
|
|
254
|
+
accessExpiresAt: issued.accessExpiresAt,
|
|
255
|
+
userId: linked.userId,
|
|
256
|
+
};
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const linkStart = flow("auth.oauthLinkStart", {
|
|
261
|
+
plane: "user",
|
|
262
|
+
in: StartIn,
|
|
263
|
+
out: StartOut,
|
|
264
|
+
errors: { AuthFailed, AuthRateLimited },
|
|
265
|
+
effects: {},
|
|
266
|
+
do: async (input, fx) => {
|
|
267
|
+
const currentUserId = fx.auth.userId;
|
|
268
|
+
if (!currentUserId) return fail("AuthFailed", { reason: "unauthenticated" });
|
|
269
|
+
const p = providers.find((c) => c.id === input.provider);
|
|
270
|
+
if (!p) return fail("AuthFailed", { reason: "provider_disabled" });
|
|
271
|
+
const redirectUri = resolveRedirectUri(p, options);
|
|
272
|
+
const clientId = p.config.clientId;
|
|
273
|
+
if (!redirectUri || !clientId) {
|
|
274
|
+
return fail("AuthFailed", { reason: "provider_misconfigured" });
|
|
275
|
+
}
|
|
276
|
+
const state = generateState();
|
|
277
|
+
const { verifier, challenge } = await mintPkcePair();
|
|
278
|
+
const nonce = p.driver.kind === "oidc" ? generateState() : undefined;
|
|
279
|
+
await putOauthFlow(
|
|
280
|
+
verifications,
|
|
281
|
+
{
|
|
282
|
+
provider: p.id,
|
|
283
|
+
stateHash: await hashChallenge(state),
|
|
284
|
+
codeVerifier: verifier,
|
|
285
|
+
redirectUri,
|
|
286
|
+
...(nonce !== undefined ? { nonce } : {}),
|
|
287
|
+
expectedIssuer: p.driver.authorizationServerId,
|
|
288
|
+
clientId,
|
|
289
|
+
currentUserId,
|
|
290
|
+
},
|
|
291
|
+
now(),
|
|
292
|
+
);
|
|
293
|
+
return {
|
|
294
|
+
provider: p.id,
|
|
295
|
+
authorizationUrl: p.driver.buildAuthorizeUrl({
|
|
296
|
+
clientId,
|
|
297
|
+
redirectUri,
|
|
298
|
+
scopes: p.config.scopes ?? [],
|
|
299
|
+
state,
|
|
300
|
+
codeChallenge: challenge,
|
|
301
|
+
...(nonce !== undefined ? { nonce } : {}),
|
|
302
|
+
}).url,
|
|
303
|
+
expiresInMs: OAUTH_FLOW_TTL_MS,
|
|
304
|
+
};
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const def = plugin("oauth", {
|
|
309
|
+
version: "0.0.1",
|
|
310
|
+
config: { providers: providers.map((p) => p.id) },
|
|
311
|
+
})
|
|
312
|
+
.needs("auth")
|
|
313
|
+
.flow(start)
|
|
314
|
+
.flow(callback)
|
|
315
|
+
.flow(linkStart);
|
|
316
|
+
|
|
317
|
+
def.binding(bindPublicAuth("/oauth/:provider/start", start, "otp"));
|
|
318
|
+
for (const b of bindCallbackBothMethods(callback)) def.binding(b);
|
|
319
|
+
def.binding(bindSessionAuth("/oauth/:provider/link", linkStart));
|
|
320
|
+
|
|
321
|
+
for (const contract of vaultContracts(providers)) def.vault(contract);
|
|
322
|
+
|
|
323
|
+
return def;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* ------------------------------------------------------------------ */
|
|
327
|
+
/* Internals */
|
|
328
|
+
/* ------------------------------------------------------------------ */
|
|
329
|
+
|
|
330
|
+
const OAUTH_FLOW_TTL_MS = 10 * 60_000;
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Bind the callback flow to both GET (standard `?code=` redirects) and POST
|
|
334
|
+
* (Apple `response_mode=form_post`) on the same per-provider path.
|
|
335
|
+
*
|
|
336
|
+
* @param flowDef - Callback flow
|
|
337
|
+
*/
|
|
338
|
+
function bindCallbackBothMethods(flowDef: AnyFlowDef): Binding[] {
|
|
339
|
+
const gates = authPublicGates("otp");
|
|
340
|
+
const getBinding = bindPublicAuthGet("/auth/oauth/callback/:provider", flowDef, gates);
|
|
341
|
+
const postBinding = bindAuthHttp(
|
|
342
|
+
http
|
|
343
|
+
.post("/auth/oauth/callback/:provider")
|
|
344
|
+
.public()
|
|
345
|
+
.gate(...gates),
|
|
346
|
+
flowDef,
|
|
347
|
+
);
|
|
348
|
+
return [getBinding, postBinding];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function buildProviders(options: OAuthOptions): ReadonlyArray<ProviderRuntime> {
|
|
352
|
+
const out: ProviderRuntime[] = [];
|
|
353
|
+
for (const id of OAUTH_PROVIDER_IDS) {
|
|
354
|
+
const config = options.providers?.[id];
|
|
355
|
+
if (!config?.enabled) continue;
|
|
356
|
+
const common = {
|
|
357
|
+
...(config.scopes !== undefined ? { scopes: config.scopes } : {}),
|
|
358
|
+
...(options.fetch !== undefined ? { fetch: options.fetch } : {}),
|
|
359
|
+
};
|
|
360
|
+
let driver: OAuthDriver;
|
|
361
|
+
switch (id) {
|
|
362
|
+
case "google":
|
|
363
|
+
driver = openOAuthGoogle(common);
|
|
364
|
+
break;
|
|
365
|
+
case "apple":
|
|
366
|
+
driver = openOAuthApple({
|
|
367
|
+
...common,
|
|
368
|
+
...(config.teamId !== undefined ? { teamId: config.teamId } : {}),
|
|
369
|
+
...(config.keyId !== undefined ? { keyId: config.keyId } : {}),
|
|
370
|
+
});
|
|
371
|
+
break;
|
|
372
|
+
case "microsoft":
|
|
373
|
+
driver = openOAuthMicrosoft({
|
|
374
|
+
...common,
|
|
375
|
+
...(config.tenant !== undefined ? { tenant: config.tenant } : {}),
|
|
376
|
+
});
|
|
377
|
+
break;
|
|
378
|
+
case "github":
|
|
379
|
+
driver = openOAuthGithub(common);
|
|
380
|
+
break;
|
|
381
|
+
case "discord":
|
|
382
|
+
driver = openOAuthDiscord(common);
|
|
383
|
+
break;
|
|
384
|
+
case "x":
|
|
385
|
+
driver = openOAuthX(common);
|
|
386
|
+
break;
|
|
387
|
+
case "facebook":
|
|
388
|
+
driver = openOAuthFacebook(common);
|
|
389
|
+
break;
|
|
390
|
+
case "figma":
|
|
391
|
+
driver = openOAuthFigma(common);
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
out.push({ id, driver, config });
|
|
395
|
+
}
|
|
396
|
+
return out;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Exact registered redirect URI — explicit config wins, else synthesized
|
|
401
|
+
* from `baseUrl` using this plugin's canonical per-provider callback path.
|
|
402
|
+
*/
|
|
403
|
+
function resolveRedirectUri(p: ProviderRuntime, options: OAuthOptions): string | undefined {
|
|
404
|
+
if (p.config.redirectUri !== undefined) return p.config.redirectUri;
|
|
405
|
+
if (options.baseUrl !== undefined) {
|
|
406
|
+
return `${options.baseUrl.replace(/\/$/, "")}/auth/oauth/callback/${p.id}`;
|
|
407
|
+
}
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Vault path holding the sealed per-user provider-token envelope. */
|
|
412
|
+
function tokenVaultPath(provider: OAuthProviderId): string {
|
|
413
|
+
return `oauth/tokens/${provider}`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Secret refs the callback may read/write (declared up front). */
|
|
417
|
+
function callbackSecretRefs(providers: ReadonlyArray<ProviderRuntime>): string[] {
|
|
418
|
+
const refs = new Set<string>();
|
|
419
|
+
for (const p of providers) {
|
|
420
|
+
const upper = p.id.toUpperCase();
|
|
421
|
+
if (p.id === "apple") refs.add("OAUTH_APPLE_PRIVATE_KEY");
|
|
422
|
+
else refs.add(`OAUTH_${upper}_CLIENT_SECRET`);
|
|
423
|
+
if (p.config.clientId === undefined) refs.add(`OAUTH_${upper}_CLIENT_ID`);
|
|
424
|
+
if (p.config.storeProviderTokens === true) refs.add(tokenVaultPath(p.id));
|
|
425
|
+
}
|
|
426
|
+
return [...refs];
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function vaultContracts(providers: ReadonlyArray<ProviderRuntime>) {
|
|
430
|
+
const contracts = [];
|
|
431
|
+
for (const p of providers) {
|
|
432
|
+
if (p.id === "apple") {
|
|
433
|
+
contracts.push(
|
|
434
|
+
vault.secret("OAUTH_APPLE_PRIVATE_KEY", {
|
|
435
|
+
description: "Sign in with Apple team private key (PKCS#8 PEM)",
|
|
436
|
+
sensitive: true,
|
|
437
|
+
}),
|
|
438
|
+
);
|
|
439
|
+
} else {
|
|
440
|
+
contracts.push(
|
|
441
|
+
vault.secret(`OAUTH_${p.id.toUpperCase()}_CLIENT_SECRET`, {
|
|
442
|
+
description: `${p.id} OAuth client secret`,
|
|
443
|
+
sensitive: true,
|
|
444
|
+
}),
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return contracts;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Extract one urlencoded field from a form_post body string. */
|
|
452
|
+
function fieldFromBody(body: unknown, field: string): string | undefined {
|
|
453
|
+
if (typeof body !== "string") return undefined;
|
|
454
|
+
return new URLSearchParams(body).get(field) ?? undefined;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
interface ClientCredentials {
|
|
458
|
+
readonly clientSecret?: string;
|
|
459
|
+
readonly clientSecretJwt?: () => Promise<string>;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Resolve client credential material at the Vault boundary (the one place
|
|
464
|
+
* `Redacted.reveal()` is allowed).
|
|
465
|
+
*/
|
|
466
|
+
async function loadCredentials(p: ProviderRuntime, fx: Fx): Promise<ClientCredentials> {
|
|
467
|
+
if (p.id === "apple") {
|
|
468
|
+
const pem = (await fx.vault.get("OAUTH_APPLE_PRIVATE_KEY")).reveal();
|
|
469
|
+
return {
|
|
470
|
+
clientSecretJwt: async () =>
|
|
471
|
+
createAppleClientSecretJwt({
|
|
472
|
+
privateKeyPem: pem,
|
|
473
|
+
teamId: p.config.teamId!,
|
|
474
|
+
keyId: p.config.keyId!,
|
|
475
|
+
clientId: p.config.clientId!,
|
|
476
|
+
}),
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
if (p.id === "x") {
|
|
480
|
+
// X authorization-code+PKCE is a public client — no secret on the call.
|
|
481
|
+
return {};
|
|
482
|
+
}
|
|
483
|
+
const upper = p.id.toUpperCase();
|
|
484
|
+
const secret = (await fx.vault.get(`OAUTH_${upper}_CLIENT_SECRET`)).reveal();
|
|
485
|
+
return { clientSecret: secret };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function credentialFailureReason(err: unknown): string {
|
|
489
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
490
|
+
if (message.includes("missing") || message.includes("not found")) return "secret_unavailable";
|
|
491
|
+
return "secret_unavailable";
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Envelope-seal provider tokens per user into one static vault path.
|
|
496
|
+
* Capability sets are exact-match, so dynamic per-user paths cannot be
|
|
497
|
+
* declared — per-user isolation lives inside the sealed envelope.
|
|
498
|
+
*/
|
|
499
|
+
async function persistProviderTokens(
|
|
500
|
+
provider: OAuthProviderId,
|
|
501
|
+
userId: string,
|
|
502
|
+
tokens: OAuthTokenSet,
|
|
503
|
+
sealSecret: string,
|
|
504
|
+
fx: Fx,
|
|
505
|
+
): Promise<void> {
|
|
506
|
+
const path = tokenVaultPath(provider);
|
|
507
|
+
let envelope: Record<string, string> = {};
|
|
508
|
+
try {
|
|
509
|
+
envelope = await unsealTokenEnvelope(sealSecret, (await fx.vault.get(path)).reveal());
|
|
510
|
+
} catch {
|
|
511
|
+
envelope = {};
|
|
512
|
+
}
|
|
513
|
+
const sanitized = stripRaw(tokens);
|
|
514
|
+
envelope[userId] = await sealTokenEnvelope(sealSecret, JSON.stringify(sanitized));
|
|
515
|
+
await fx.vault.set(path, JSON.stringify(envelope));
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** Drop the raw provider response before persisting. */
|
|
519
|
+
function stripRaw(tokens: OAuthTokenSet): Omit<OAuthTokenSet, "raw"> {
|
|
520
|
+
return {
|
|
521
|
+
accessToken: tokens.accessToken,
|
|
522
|
+
...(tokens.refreshToken !== undefined ? { refreshToken: tokens.refreshToken } : {}),
|
|
523
|
+
...(tokens.expiresAt !== undefined ? { expiresAt: tokens.expiresAt } : {}),
|
|
524
|
+
...(tokens.scopes !== undefined ? { scopes: tokens.scopes } : {}),
|
|
525
|
+
...(tokens.idToken !== undefined ? { idToken: tokens.idToken } : {}),
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function protocolReason(err: unknown): string {
|
|
530
|
+
const reason = (err as { reason?: unknown } | null)?.reason;
|
|
531
|
+
return typeof reason === "string" ? reason : "exchange_failed";
|
|
532
|
+
}
|
package/src/plugins/otp.ts
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
import { constantTimeEqual } from "../auth/constant-time.ts";
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
IdentityError,
|
|
9
|
+
linkOrProvision,
|
|
10
10
|
normalizeEmail,
|
|
11
11
|
type IdentityStore,
|
|
12
|
+
type UserIdentityRow,
|
|
12
13
|
} from "../auth/identity.ts";
|
|
13
14
|
import type { OtpPluginConfig } from "../auth/otp-capability.ts";
|
|
14
15
|
import { sealOtp, unsealOtp } from "../auth/otp-seal.ts";
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
fail,
|
|
36
37
|
flow,
|
|
37
38
|
resolveMethodSecret,
|
|
39
|
+
resolveSharedIdentities,
|
|
38
40
|
z,
|
|
39
41
|
type AuthMethodOptions,
|
|
40
42
|
} from "./auth/shared.ts";
|
|
@@ -198,7 +200,7 @@ export function otp(opts: OtpOptions): PluginDef {
|
|
|
198
200
|
const runtime = createMethodRuntime(opts);
|
|
199
201
|
const secret = resolveMethodSecret(opts);
|
|
200
202
|
const phones = opts.phones ?? createPhoneStore();
|
|
201
|
-
const identities = opts
|
|
203
|
+
const identities = resolveSharedIdentities(opts);
|
|
202
204
|
const verifications = opts.verifications ?? createVerificationStore();
|
|
203
205
|
const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
204
206
|
const resendCooldownMs =
|
|
@@ -319,7 +321,19 @@ export function otp(opts: OtpOptions): PluginDef {
|
|
|
319
321
|
|
|
320
322
|
let userId = phones.byPhone.get(phone);
|
|
321
323
|
if (!userId) {
|
|
322
|
-
|
|
324
|
+
try {
|
|
325
|
+
const { user } = await linkOrProvision(identities, {
|
|
326
|
+
provider: "otp",
|
|
327
|
+
providerAccountId: phone,
|
|
328
|
+
now: () => now,
|
|
329
|
+
});
|
|
330
|
+
userId = user.id;
|
|
331
|
+
} catch (err) {
|
|
332
|
+
if (err instanceof IdentityError) {
|
|
333
|
+
return fail("AuthFailed", { reason: "invalid_credentials" });
|
|
334
|
+
}
|
|
335
|
+
throw err;
|
|
336
|
+
}
|
|
323
337
|
phones.byPhone.set(phone, userId);
|
|
324
338
|
}
|
|
325
339
|
const issued = await issueSessionWithScopes(runtime.sessions, runtime.crypto, {
|
|
@@ -518,7 +532,19 @@ export function otp(opts: OtpOptions): PluginDef {
|
|
|
518
532
|
if (phone) {
|
|
519
533
|
let userId = phones.byPhone.get(phone);
|
|
520
534
|
if (!userId) {
|
|
521
|
-
|
|
535
|
+
try {
|
|
536
|
+
const { user } = await linkOrProvision(identities, {
|
|
537
|
+
provider: "otp",
|
|
538
|
+
providerAccountId: phone,
|
|
539
|
+
now: () => now,
|
|
540
|
+
});
|
|
541
|
+
userId = user.id;
|
|
542
|
+
} catch (err) {
|
|
543
|
+
if (err instanceof IdentityError) {
|
|
544
|
+
return fail("AuthFailed", { reason: "invalid_credentials" });
|
|
545
|
+
}
|
|
546
|
+
throw err;
|
|
547
|
+
}
|
|
522
548
|
phones.byPhone.set(phone, userId);
|
|
523
549
|
}
|
|
524
550
|
const issued = await issueSessionWithScopes(runtime.sessions, runtime.crypto, {
|
|
@@ -534,7 +560,23 @@ export function otp(opts: OtpOptions): PluginDef {
|
|
|
534
560
|
};
|
|
535
561
|
}
|
|
536
562
|
|
|
537
|
-
|
|
563
|
+
let user: UserIdentityRow;
|
|
564
|
+
try {
|
|
565
|
+
user = (
|
|
566
|
+
await linkOrProvision(identities, {
|
|
567
|
+
provider: "otp",
|
|
568
|
+
providerAccountId: email!,
|
|
569
|
+
email,
|
|
570
|
+
emailVerified: true,
|
|
571
|
+
now: () => now,
|
|
572
|
+
})
|
|
573
|
+
).user;
|
|
574
|
+
} catch (err) {
|
|
575
|
+
if (err instanceof IdentityError) {
|
|
576
|
+
return fail("AuthFailed", { reason: "invalid_credentials" });
|
|
577
|
+
}
|
|
578
|
+
throw err;
|
|
579
|
+
}
|
|
538
580
|
const issued = await issueSessionWithScopes(runtime.sessions, runtime.crypto, {
|
|
539
581
|
id: user.id,
|
|
540
582
|
plane: "user",
|
package/src/plugins/passkey.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { constantTimeEqual } from "../auth/constant-time.ts";
|
|
10
|
+
import { IdentityError, linkOrProvision } from "../auth/identity.ts";
|
|
10
11
|
import {
|
|
11
12
|
createVerificationStore,
|
|
12
13
|
hashChallenge,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
createMethodRuntime,
|
|
25
26
|
fail,
|
|
26
27
|
flow,
|
|
28
|
+
resolveSharedIdentities,
|
|
27
29
|
z,
|
|
28
30
|
type AuthMethodOptions,
|
|
29
31
|
} from "./auth/shared.ts";
|
|
@@ -82,6 +84,7 @@ const CeremonyIn = z.object({
|
|
|
82
84
|
*/
|
|
83
85
|
export function passkey(opts: PasskeyOptions = {}): PluginDef {
|
|
84
86
|
const runtime = createMethodRuntime(opts);
|
|
87
|
+
const identities = resolveSharedIdentities(opts);
|
|
85
88
|
const passkeys = opts.passkeys ?? createPasskeyStore();
|
|
86
89
|
const challenges = opts.challenges ?? createVerificationStore();
|
|
87
90
|
const rpId = opts.rpId ?? "localhost";
|
|
@@ -152,9 +155,25 @@ export function passkey(opts: PasskeyOptions = {}): PluginDef {
|
|
|
152
155
|
if (passkeys.byCredentialId.has(input.credentialId)) {
|
|
153
156
|
return fail("AuthFailed", { reason: "invalid_credentials" });
|
|
154
157
|
}
|
|
158
|
+
let userId: string;
|
|
159
|
+
try {
|
|
160
|
+
userId = (
|
|
161
|
+
await linkOrProvision(identities, {
|
|
162
|
+
provider: "passkey",
|
|
163
|
+
providerAccountId: input.credentialId,
|
|
164
|
+
currentUserId: sessionUser,
|
|
165
|
+
now: runtime.now,
|
|
166
|
+
})
|
|
167
|
+
).user.id;
|
|
168
|
+
} catch (err) {
|
|
169
|
+
if (err instanceof IdentityError) {
|
|
170
|
+
return fail("AuthFailed", { reason: "invalid_credentials" });
|
|
171
|
+
}
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
155
174
|
const cred: PasskeyCredential = {
|
|
156
175
|
credentialId: input.credentialId,
|
|
157
|
-
userId
|
|
176
|
+
userId,
|
|
158
177
|
publicKey: input.publicKey,
|
|
159
178
|
counter: verified.signCount,
|
|
160
179
|
createdAt: runtime.now(),
|