@apifuse/provider-sdk 2.1.0-beta.2 → 2.1.0-beta.21
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/AUTHORING.md +330 -8
- package/CHANGELOG.md +89 -1
- package/README.md +64 -17
- package/SUBMISSION.md +86 -0
- package/bin/apifuse-check.ts +60 -6
- package/bin/apifuse-dev.ts +58 -8
- package/bin/apifuse-pack-check.ts +32 -2
- package/bin/apifuse-pack-smoke.ts +133 -6
- package/bin/apifuse-perf.ts +142 -49
- package/bin/apifuse-record.ts +182 -104
- package/bin/apifuse-submit-check.ts +3243 -0
- package/bin/apifuse.ts +1 -1
- package/dist/auth.d.ts +76 -0
- package/dist/auth.js +436 -0
- package/dist/ceremonies/index.d.ts +41 -0
- package/dist/ceremonies/index.js +490 -0
- package/dist/choice-token.d.ts +24 -0
- package/dist/choice-token.js +74 -0
- package/dist/cli/commands.d.ts +10 -0
- package/dist/cli/commands.js +80 -0
- package/dist/cli/create.d.ts +47 -0
- package/dist/cli/create.js +777 -0
- package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
- package/dist/cli/templates/provider/.gitignore.tpl +22 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +87 -0
- package/dist/cli/templates/provider/CLAUDE.md.tpl +1 -0
- package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
- package/dist/cli/templates/provider/README.md.tpl +163 -0
- package/dist/cli/templates/provider/dev.ts.tpl +5 -0
- package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
- package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
- package/dist/cli/templates/provider/index.ts.tpl +15 -0
- package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
- package/dist/cli/templates/provider/meta.ts.tpl +7 -0
- package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
- package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
- package/dist/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
- package/dist/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
- package/dist/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
- package/dist/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
- package/dist/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
- package/dist/cli/templates/provider/start.ts.tpl +5 -0
- package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
- package/dist/config/loader.d.ts +107 -0
- package/dist/config/loader.js +935 -0
- package/dist/contract-json.d.ts +9 -0
- package/dist/contract-json.js +51 -0
- package/dist/contract-serialization.d.ts +4 -0
- package/dist/contract-serialization.js +78 -0
- package/dist/contract-types.d.ts +49 -0
- package/dist/contract-types.js +1 -0
- package/dist/contract.d.ts +6 -0
- package/dist/contract.js +156 -0
- package/dist/define.d.ts +100 -0
- package/dist/define.js +1383 -0
- package/dist/dev.d.ts +9 -0
- package/dist/dev.js +15 -0
- package/dist/errors.d.ts +59 -0
- package/dist/errors.js +97 -0
- package/dist/i18n/catalog.d.ts +29 -0
- package/dist/i18n/catalog.js +159 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/i18n/index.js +2 -0
- package/dist/i18n/keys.d.ts +10 -0
- package/dist/i18n/keys.js +34 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +38 -0
- package/dist/lint.d.ts +74 -0
- package/dist/lint.js +729 -0
- package/dist/observability.d.ts +5 -0
- package/dist/observability.js +39 -0
- package/dist/provider.d.ts +11 -0
- package/dist/provider.js +9 -0
- package/dist/public-schema-field-lint.d.ts +2 -0
- package/dist/public-schema-field-lint.js +158 -0
- package/dist/recipes/gov-api.d.ts +19 -0
- package/dist/recipes/gov-api.js +72 -0
- package/dist/recipes/rest-api.d.ts +21 -0
- package/dist/recipes/rest-api.js +115 -0
- package/dist/runtime/auth-flow.d.ts +14 -0
- package/dist/runtime/auth-flow.js +46 -0
- package/dist/runtime/browser.d.ts +25 -0
- package/dist/runtime/browser.js +1237 -0
- package/dist/runtime/cache.d.ts +10 -0
- package/dist/runtime/cache.js +372 -0
- package/dist/runtime/choice.d.ts +15 -0
- package/dist/runtime/choice.js +435 -0
- package/dist/runtime/credential.d.ts +8 -0
- package/dist/runtime/credential.js +61 -0
- package/dist/runtime/env.d.ts +2 -0
- package/dist/runtime/env.js +10 -0
- package/dist/runtime/executor.d.ts +16 -0
- package/dist/runtime/executor.js +51 -0
- package/dist/runtime/http.d.ts +8 -0
- package/dist/runtime/http.js +726 -0
- package/dist/runtime/insights.d.ts +9 -0
- package/dist/runtime/insights.js +324 -0
- package/dist/runtime/instrumentation.d.ts +8 -0
- package/dist/runtime/instrumentation.js +269 -0
- package/dist/runtime/key-derivation.d.ts +24 -0
- package/dist/runtime/key-derivation.js +73 -0
- package/dist/runtime/keyring.d.ts +25 -0
- package/dist/runtime/keyring.js +93 -0
- package/dist/runtime/namespace.d.ts +9 -0
- package/dist/runtime/namespace.js +19 -0
- package/dist/runtime/otlp.d.ts +39 -0
- package/dist/runtime/otlp.js +103 -0
- package/dist/runtime/perf.d.ts +12 -0
- package/dist/runtime/perf.js +52 -0
- package/dist/runtime/prevalidate.d.ts +12 -0
- package/dist/runtime/prevalidate.js +173 -0
- package/dist/runtime/provider.d.ts +2 -0
- package/dist/runtime/provider.js +11 -0
- package/dist/runtime/proxy-errors.d.ts +21 -0
- package/dist/runtime/proxy-errors.js +83 -0
- package/dist/runtime/proxy-telemetry.d.ts +8 -0
- package/dist/runtime/proxy-telemetry.js +174 -0
- package/dist/runtime/redis.d.ts +17 -0
- package/dist/runtime/redis.js +82 -0
- package/dist/runtime/request-options.d.ts +3 -0
- package/dist/runtime/request-options.js +42 -0
- package/dist/runtime/state.d.ts +17 -0
- package/dist/runtime/state.js +344 -0
- package/dist/runtime/stealth.d.ts +21 -0
- package/dist/runtime/stealth.js +980 -0
- package/dist/runtime/stt.d.ts +22 -0
- package/dist/runtime/stt.js +480 -0
- package/dist/runtime/trace.d.ts +26 -0
- package/dist/runtime/trace.js +142 -0
- package/dist/runtime/waterfall.d.ts +12 -0
- package/dist/runtime/waterfall.js +147 -0
- package/dist/schema.d.ts +74 -0
- package/dist/schema.js +243 -0
- package/dist/serve.d.ts +1 -0
- package/dist/serve.js +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +2 -0
- package/dist/server/serve.d.ts +64 -0
- package/dist/server/serve.js +1118 -0
- package/dist/server/types.d.ts +136 -0
- package/dist/server/types.js +86 -0
- package/dist/stealth/profiles.d.ts +4 -0
- package/dist/stealth/profiles.js +259 -0
- package/dist/stream.d.ts +44 -0
- package/dist/stream.js +151 -0
- package/dist/testing/helpers.d.ts +23 -0
- package/dist/testing/helpers.js +95 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +2 -0
- package/dist/testing/run.d.ts +34 -0
- package/dist/testing/run.js +307 -0
- package/dist/types.d.ts +1467 -0
- package/dist/types.js +61 -0
- package/dist/utils/date.d.ts +6 -0
- package/dist/utils/date.js +101 -0
- package/dist/utils/parse.d.ts +16 -0
- package/dist/utils/parse.js +51 -0
- package/dist/utils/text.d.ts +4 -0
- package/dist/utils/text.js +14 -0
- package/dist/utils/transform.d.ts +8 -0
- package/dist/utils/transform.js +48 -0
- package/package.json +57 -29
- package/src/auth.ts +786 -0
- package/src/ceremonies/index.ts +8 -2
- package/src/choice-token.ts +165 -0
- package/src/cli/commands.ts +34 -11
- package/src/cli/create.ts +254 -128
- package/src/cli/templates/provider/.dockerignore.tpl +22 -0
- package/src/cli/templates/provider/.gitignore.tpl +22 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +87 -0
- package/src/cli/templates/provider/CLAUDE.md.tpl +1 -0
- package/src/cli/templates/provider/README.md.tpl +87 -7
- package/src/cli/templates/provider/dev.ts.tpl +1 -1
- package/src/cli/templates/provider/domain/README.md.tpl +3 -0
- package/src/cli/templates/provider/index.ts.tpl +5 -47
- package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
- package/src/cli/templates/provider/meta.ts.tpl +7 -0
- package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
- package/src/cli/templates/provider/operations/ping.ts.tpl +24 -0
- package/src/cli/templates/provider/schemas/ping.ts.tpl +24 -0
- package/src/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
- package/src/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
- package/src/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
- package/src/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
- package/src/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
- package/src/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
- package/src/cli/templates/provider/start.ts.tpl +1 -1
- package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
- package/src/config/loader.ts +1224 -9
- package/src/contract-json.ts +75 -0
- package/src/contract-serialization.ts +89 -0
- package/src/contract-types.ts +52 -0
- package/src/contract.ts +216 -0
- package/src/define.ts +1820 -70
- package/src/errors.ts +27 -0
- package/src/i18n/catalog.ts +277 -0
- package/src/i18n/index.ts +2 -0
- package/src/i18n/keys.ts +64 -0
- package/src/index.ts +189 -9
- package/src/lint.ts +580 -73
- package/src/observability.ts +41 -0
- package/src/provider.ts +131 -4
- package/src/public-schema-field-lint.ts +237 -0
- package/src/runtime/auth-flow.ts +9 -0
- package/src/runtime/browser.ts +1054 -51
- package/src/runtime/cache.ts +528 -0
- package/src/runtime/choice.ts +760 -0
- package/src/runtime/executor.ts +32 -3
- package/src/runtime/http.ts +980 -195
- package/src/runtime/insights.ts +11 -11
- package/src/runtime/instrumentation.ts +12 -4
- package/src/runtime/key-derivation.ts +1 -1
- package/src/runtime/keyring.ts +4 -3
- package/src/runtime/proxy-errors.ts +132 -0
- package/src/runtime/proxy-telemetry.ts +253 -0
- package/src/runtime/redis.ts +116 -0
- package/src/runtime/request-options.ts +66 -0
- package/src/runtime/state.ts +563 -0
- package/src/runtime/stealth.ts +1336 -0
- package/src/runtime/stt.ts +629 -0
- package/src/runtime/trace.ts +1 -1
- package/src/schema.ts +363 -1
- package/src/server/serve.ts +1192 -75
- package/src/server/types.ts +37 -0
- package/src/stream.ts +210 -0
- package/src/testing/run.ts +40 -6
- package/src/types.ts +1283 -59
- package/src/runtime/tls.ts +0 -434
- package/src/types/playwright-stealth.d.ts +0 -9
package/src/auth.ts
ADDED
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
import { AuthError, ProviderError } from "./errors";
|
|
2
|
+
import type {
|
|
3
|
+
AuthAbortData,
|
|
4
|
+
AuthConfig,
|
|
5
|
+
AuthFlowTerminalContext,
|
|
6
|
+
AuthSafeData,
|
|
7
|
+
AuthSafeJson,
|
|
8
|
+
AuthTurn,
|
|
9
|
+
ContextDeclaration,
|
|
10
|
+
CredentialDeclaration,
|
|
11
|
+
FlowContext,
|
|
12
|
+
ProviderLocaleKeyInput,
|
|
13
|
+
} from "./types";
|
|
14
|
+
|
|
15
|
+
const CREDENTIALS_AUTH_CHALLENGE_CONTEXT_KEY = "__credentialsAuthChallenge";
|
|
16
|
+
const DEFAULT_COMPLETE_TURN_ID = "auth.complete";
|
|
17
|
+
const DEFAULT_ABORT_TURN_ID = "auth.abort";
|
|
18
|
+
const DEFAULT_FORM_TURN_ID = "auth.form";
|
|
19
|
+
const DEFAULT_POLL_TURN_ID = "auth.poll";
|
|
20
|
+
const SENSITIVE_ABORT_DATA_KEY_PATTERN =
|
|
21
|
+
/(authorization|cookie|credential|header|html|password|secret|session|token|apikey)/i;
|
|
22
|
+
|
|
23
|
+
function normalizedAuthDataKey(key: string): string {
|
|
24
|
+
return key.replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isPlainAuthJsonObject(value: object): boolean {
|
|
28
|
+
const prototype = Object.getPrototypeOf(value);
|
|
29
|
+
return prototype === Object.prototype || prototype === null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isSensitiveAuthDataKey(key: string): boolean {
|
|
33
|
+
return SENSITIVE_ABORT_DATA_KEY_PATTERN.test(normalizedAuthDataKey(key));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type CredentialsAuthFieldType =
|
|
37
|
+
| "string"
|
|
38
|
+
| "email"
|
|
39
|
+
| "password"
|
|
40
|
+
| "otp";
|
|
41
|
+
|
|
42
|
+
export interface CredentialsAuthField {
|
|
43
|
+
type?: CredentialsAuthFieldType;
|
|
44
|
+
labelKey?: ProviderLocaleKeyInput;
|
|
45
|
+
descriptionKey?: ProviderLocaleKeyInput;
|
|
46
|
+
placeholderKey?: ProviderLocaleKeyInput;
|
|
47
|
+
required?: boolean;
|
|
48
|
+
/** Marks field values as secret UI/input material, for example passwords or OTPs. */
|
|
49
|
+
sensitive?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type CredentialsAuthFields = Record<string, CredentialsAuthField>;
|
|
53
|
+
|
|
54
|
+
export type CredentialsAuthInput<TFields extends CredentialsAuthFields> = {
|
|
55
|
+
[K in keyof TFields]: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type CredentialsAuthCredential<TCredentialKeys extends readonly string[]> = {
|
|
59
|
+
[K in TCredentialKeys[number]]: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function assertSafeAuthJson(value: AuthSafeJson, path: string): void {
|
|
63
|
+
if (value === null) return;
|
|
64
|
+
if (typeof value === "string" || typeof value === "boolean") return;
|
|
65
|
+
if (typeof value === "number") {
|
|
66
|
+
if (Number.isFinite(value)) return;
|
|
67
|
+
throw new AuthError(`Auth abort ${path} must be a finite number`, {
|
|
68
|
+
code: "auth_abort_unsafe_data",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (Array.isArray(value)) {
|
|
72
|
+
for (const [index, item] of value.entries()) {
|
|
73
|
+
assertSafeAuthJson(item, `${path}[${index}]`);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (value instanceof Error) {
|
|
78
|
+
throw new AuthError(`Auth abort ${path} must not include Error objects`, {
|
|
79
|
+
code: "auth_abort_unsafe_data",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (typeof value !== "object") {
|
|
83
|
+
throw new AuthError(`Auth abort ${path} must be JSON-safe`, {
|
|
84
|
+
code: "auth_abort_unsafe_data",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (!isPlainAuthJsonObject(value)) {
|
|
88
|
+
throw new AuthError(`Auth abort ${path} must be a plain JSON object`, {
|
|
89
|
+
code: "auth_abort_unsafe_data",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
for (const [key, item] of Object.entries(value)) {
|
|
93
|
+
if (isSensitiveAuthDataKey(key)) {
|
|
94
|
+
throw new AuthError(`Auth abort ${path}.${key} must not include secrets`, {
|
|
95
|
+
code: "auth_abort_unsafe_data",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
assertSafeAuthJson(item, `${path}.${key}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function assertSafeAuthData(data: AuthSafeData, path: string): void {
|
|
103
|
+
assertSafeAuthJson(data, path);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function authTurnBase(options: {
|
|
107
|
+
turnId: string | undefined;
|
|
108
|
+
defaultTurnId: string;
|
|
109
|
+
expiresAt?: string;
|
|
110
|
+
}): Pick<AuthTurn, "turnId" | "expiresAt"> {
|
|
111
|
+
return {
|
|
112
|
+
turnId: options.turnId ?? options.defaultTurnId,
|
|
113
|
+
...(options.expiresAt ? { expiresAt: options.expiresAt } : {}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function abortData(options: {
|
|
118
|
+
code: string;
|
|
119
|
+
message?: string;
|
|
120
|
+
retry?: AuthAbortData["retry"];
|
|
121
|
+
actionHint?: AuthSafeJson;
|
|
122
|
+
fieldErrors?: { readonly [field: string]: string };
|
|
123
|
+
data?: AuthSafeData;
|
|
124
|
+
}): AuthAbortData {
|
|
125
|
+
if (options.actionHint !== undefined) {
|
|
126
|
+
assertSafeAuthJson(options.actionHint, "actionHint");
|
|
127
|
+
}
|
|
128
|
+
if (options.data !== undefined) {
|
|
129
|
+
assertSafeAuthData(options.data, "data");
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
code: options.code,
|
|
133
|
+
...(options.message ? { message: options.message } : {}),
|
|
134
|
+
...(options.retry ? { retry: options.retry } : {}),
|
|
135
|
+
...(options.actionHint !== undefined ? { actionHint: options.actionHint } : {}),
|
|
136
|
+
...(options.fieldErrors ? { fieldErrors: options.fieldErrors } : {}),
|
|
137
|
+
...(options.data ? { details: options.data } : {}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function createAuthFlowHelpers(options: {
|
|
142
|
+
readonly signal?: AbortSignal;
|
|
143
|
+
readonly deadline?: string;
|
|
144
|
+
} = {}): AuthFlowTerminalContext {
|
|
145
|
+
return {
|
|
146
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
147
|
+
...(options.deadline ? { deadline: options.deadline } : {}),
|
|
148
|
+
complete({ credential, metadata, data, turnId, expiresAt }) {
|
|
149
|
+
return {
|
|
150
|
+
kind: "complete",
|
|
151
|
+
...authTurnBase({ turnId, defaultTurnId: DEFAULT_COMPLETE_TURN_ID, expiresAt }),
|
|
152
|
+
data: {
|
|
153
|
+
...(data ?? {}),
|
|
154
|
+
credential,
|
|
155
|
+
...(metadata ? { metadata } : {}),
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
abort({
|
|
160
|
+
code,
|
|
161
|
+
message,
|
|
162
|
+
retry,
|
|
163
|
+
actionHint,
|
|
164
|
+
fieldErrors,
|
|
165
|
+
data,
|
|
166
|
+
turnId,
|
|
167
|
+
expiresAt,
|
|
168
|
+
}) {
|
|
169
|
+
return {
|
|
170
|
+
kind: "abort",
|
|
171
|
+
...authTurnBase({ turnId, defaultTurnId: DEFAULT_ABORT_TURN_ID, expiresAt }),
|
|
172
|
+
data: abortData({
|
|
173
|
+
code,
|
|
174
|
+
message,
|
|
175
|
+
retry,
|
|
176
|
+
actionHint,
|
|
177
|
+
fieldErrors,
|
|
178
|
+
data,
|
|
179
|
+
}),
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
nextForm(options) {
|
|
183
|
+
return {
|
|
184
|
+
kind: "form",
|
|
185
|
+
...authTurnBase({
|
|
186
|
+
turnId: options.turnId,
|
|
187
|
+
defaultTurnId: DEFAULT_FORM_TURN_ID,
|
|
188
|
+
expiresAt: options.expiresAt,
|
|
189
|
+
}),
|
|
190
|
+
...(options.hintKey ? { hintKey: options.hintKey } : {}),
|
|
191
|
+
...(options.timing ? { timing: options.timing } : {}),
|
|
192
|
+
...(options.data ? { data: options.data } : {}),
|
|
193
|
+
expectedInput:
|
|
194
|
+
options.expectedInput ?? expectedInputFromFields(options.fields ?? {}),
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
nextPoll(options = {}) {
|
|
198
|
+
return {
|
|
199
|
+
kind: "poll",
|
|
200
|
+
...authTurnBase({
|
|
201
|
+
turnId: options.turnId,
|
|
202
|
+
defaultTurnId: DEFAULT_POLL_TURN_ID,
|
|
203
|
+
expiresAt: options.expiresAt,
|
|
204
|
+
}),
|
|
205
|
+
...(options.hintKey ? { hintKey: options.hintKey } : {}),
|
|
206
|
+
...(options.timing ? { timing: options.timing } : {}),
|
|
207
|
+
...(options.data ? { data: options.data } : {}),
|
|
208
|
+
};
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export class AuthAbortError extends AuthError {
|
|
214
|
+
readonly turn: AuthTurn;
|
|
215
|
+
|
|
216
|
+
constructor(options: Parameters<AuthFlowTerminalContext["abort"]>[0]) {
|
|
217
|
+
super(options.message ?? options.code, {
|
|
218
|
+
code: options.code,
|
|
219
|
+
retryable: options.retry === "retry",
|
|
220
|
+
});
|
|
221
|
+
this.name = "AuthAbortError";
|
|
222
|
+
this.turn = createAuthFlowHelpers().abort(options);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface CredentialsAuthCompleteResult<
|
|
227
|
+
TCredentialKeys extends readonly string[],
|
|
228
|
+
> {
|
|
229
|
+
credential: CredentialsAuthCredential<TCredentialKeys>;
|
|
230
|
+
/** Additional non-credential auth-flow data to return alongside credential. */
|
|
231
|
+
data?: Record<string, unknown>;
|
|
232
|
+
turnId?: string;
|
|
233
|
+
expiresAt?: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface CredentialsAuthChallengeRequest<TChallengeId extends string = string> {
|
|
237
|
+
kind: "challenge";
|
|
238
|
+
challengeId: TChallengeId;
|
|
239
|
+
state?: Record<string, unknown>;
|
|
240
|
+
turnId?: string;
|
|
241
|
+
hintKey?: ProviderLocaleKeyInput;
|
|
242
|
+
expiresAt?: string;
|
|
243
|
+
data?: Record<string, unknown>;
|
|
244
|
+
timing?: AuthTurn["timing"];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export type CredentialsAuthLoginResult<
|
|
248
|
+
TCredentialKeys extends readonly string[],
|
|
249
|
+
TChallengeId extends string = string,
|
|
250
|
+
> =
|
|
251
|
+
| CredentialsAuthCompleteResult<TCredentialKeys>
|
|
252
|
+
| CredentialsAuthChallengeRequest<TChallengeId>;
|
|
253
|
+
|
|
254
|
+
export interface CredentialsAuthChallengeDefinition<
|
|
255
|
+
TFields extends CredentialsAuthFields,
|
|
256
|
+
TCredentialKeys extends readonly string[],
|
|
257
|
+
TChallengeId extends string,
|
|
258
|
+
> {
|
|
259
|
+
fields?: TFields;
|
|
260
|
+
hintKey?: ProviderLocaleKeyInput;
|
|
261
|
+
turnId?: string;
|
|
262
|
+
retryTurnId?: string;
|
|
263
|
+
pendingTurnId?: string;
|
|
264
|
+
timing?: AuthTurn["timing"];
|
|
265
|
+
verify?: (
|
|
266
|
+
ctx: FlowContext,
|
|
267
|
+
input: CredentialsAuthInput<TFields>,
|
|
268
|
+
state: Record<string, unknown>,
|
|
269
|
+
) =>
|
|
270
|
+
| CredentialsAuthLoginResult<TCredentialKeys, TChallengeId>
|
|
271
|
+
| Promise<CredentialsAuthLoginResult<TCredentialKeys, TChallengeId>>;
|
|
272
|
+
poll?: (
|
|
273
|
+
ctx: FlowContext,
|
|
274
|
+
state: Record<string, unknown>,
|
|
275
|
+
) =>
|
|
276
|
+
| CredentialsAuthLoginResult<TCredentialKeys, TChallengeId>
|
|
277
|
+
| null
|
|
278
|
+
| Promise<CredentialsAuthLoginResult<TCredentialKeys, TChallengeId> | null>;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export interface DefineCredentialsAuthOptions<
|
|
282
|
+
TFields extends CredentialsAuthFields,
|
|
283
|
+
TCredentialKeys extends readonly string[],
|
|
284
|
+
TChallenges extends Record<
|
|
285
|
+
string,
|
|
286
|
+
CredentialsAuthChallengeDefinition<
|
|
287
|
+
CredentialsAuthFields,
|
|
288
|
+
TCredentialKeys,
|
|
289
|
+
keyof TChallenges & string
|
|
290
|
+
>
|
|
291
|
+
> = Record<
|
|
292
|
+
string,
|
|
293
|
+
CredentialsAuthChallengeDefinition<CredentialsAuthFields, TCredentialKeys, string>
|
|
294
|
+
>,
|
|
295
|
+
> {
|
|
296
|
+
fields: TFields;
|
|
297
|
+
credentialKeys: TCredentialKeys;
|
|
298
|
+
storesReusableSecret?: boolean;
|
|
299
|
+
justification?: string;
|
|
300
|
+
hintKey?: ProviderLocaleKeyInput;
|
|
301
|
+
startTurnId?: string;
|
|
302
|
+
retryTurnId?: string;
|
|
303
|
+
completeTurnId?: string;
|
|
304
|
+
challenges?: TChallenges;
|
|
305
|
+
/** Extra auth-flow context keys used by custom login/challenge code. */
|
|
306
|
+
contextKeys?: readonly string[];
|
|
307
|
+
login(
|
|
308
|
+
ctx: FlowContext,
|
|
309
|
+
input: CredentialsAuthInput<TFields>,
|
|
310
|
+
):
|
|
311
|
+
| CredentialsAuthLoginResult<TCredentialKeys, keyof TChallenges & string>
|
|
312
|
+
| Promise<CredentialsAuthLoginResult<TCredentialKeys, keyof TChallenges & string>>;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export interface DefinedCredentialsAuth {
|
|
316
|
+
auth: AuthConfig;
|
|
317
|
+
credential: CredentialDeclaration;
|
|
318
|
+
context: ContextDeclaration;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
interface PendingCredentialsAuthChallenge {
|
|
322
|
+
challengeId: string;
|
|
323
|
+
state: Record<string, unknown>;
|
|
324
|
+
turnId?: string;
|
|
325
|
+
hintKey?: ProviderLocaleKeyInput;
|
|
326
|
+
expiresAt?: string;
|
|
327
|
+
data?: Record<string, unknown>;
|
|
328
|
+
timing?: AuthTurn["timing"];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function credentialsAuthChallenge<TChallengeId extends string>(
|
|
332
|
+
challengeId: TChallengeId,
|
|
333
|
+
options: Omit<CredentialsAuthChallengeRequest<TChallengeId>, "kind" | "challengeId"> = {},
|
|
334
|
+
): CredentialsAuthChallengeRequest<TChallengeId> {
|
|
335
|
+
return {
|
|
336
|
+
kind: "challenge",
|
|
337
|
+
challengeId,
|
|
338
|
+
...options,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function expectedInputFromFields(fields: CredentialsAuthFields): Record<string, unknown> {
|
|
343
|
+
return {
|
|
344
|
+
type: "object",
|
|
345
|
+
properties: Object.fromEntries(
|
|
346
|
+
Object.entries(fields).map(([name, field]) => [
|
|
347
|
+
name,
|
|
348
|
+
{
|
|
349
|
+
type: "string",
|
|
350
|
+
...(field.type === "email" ? { format: "email" } : {}),
|
|
351
|
+
...(field.type === "password" ? { format: "password" } : {}),
|
|
352
|
+
...(field.type === "otp" ? { format: "otp" } : {}),
|
|
353
|
+
...(field.labelKey ? { nameKey: field.labelKey } : {}),
|
|
354
|
+
...(field.descriptionKey
|
|
355
|
+
? { descriptionKey: field.descriptionKey }
|
|
356
|
+
: {}),
|
|
357
|
+
...(field.placeholderKey
|
|
358
|
+
? { placeholderKey: field.placeholderKey }
|
|
359
|
+
: {}),
|
|
360
|
+
...(field.sensitive || field.type === "password" || field.type === "otp"
|
|
361
|
+
? { sensitive: true }
|
|
362
|
+
: {}),
|
|
363
|
+
},
|
|
364
|
+
]),
|
|
365
|
+
),
|
|
366
|
+
required: Object.entries(fields)
|
|
367
|
+
.filter(([, field]) => field.required !== false)
|
|
368
|
+
.map(([name]) => name),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function collectMissingFields(
|
|
373
|
+
fields: CredentialsAuthFields,
|
|
374
|
+
input: Record<string, unknown> | undefined,
|
|
375
|
+
): string[] {
|
|
376
|
+
return Object.entries(fields)
|
|
377
|
+
.filter(([, field]) => field.required !== false)
|
|
378
|
+
.map(([name]) => name)
|
|
379
|
+
.filter((name) => {
|
|
380
|
+
const value = input?.[name];
|
|
381
|
+
return typeof value !== "string" || value.trim().length === 0;
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function normalizeInput<TFields extends CredentialsAuthFields>(
|
|
386
|
+
fields: TFields,
|
|
387
|
+
input: Record<string, unknown> | undefined,
|
|
388
|
+
): CredentialsAuthInput<TFields> {
|
|
389
|
+
const result: Record<string, string> = {};
|
|
390
|
+
for (const name of Object.keys(fields)) {
|
|
391
|
+
const value = input?.[name];
|
|
392
|
+
result[name] = typeof value === "string" ? value : "";
|
|
393
|
+
}
|
|
394
|
+
return result as CredentialsAuthInput<TFields>;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function assertCredentialKeys<TCredentialKeys extends readonly string[]>(
|
|
398
|
+
credentialKeys: TCredentialKeys,
|
|
399
|
+
credential: Record<string, unknown>,
|
|
400
|
+
): asserts credential is CredentialsAuthCredential<TCredentialKeys> {
|
|
401
|
+
const missing = credentialKeys.filter((key) => {
|
|
402
|
+
const value = credential[key];
|
|
403
|
+
return typeof value !== "string" || value.length === 0;
|
|
404
|
+
});
|
|
405
|
+
if (missing.length > 0) {
|
|
406
|
+
throw new ProviderError(
|
|
407
|
+
`Credentials auth login completed without required credential key(s): ${missing.join(", ")}`,
|
|
408
|
+
{
|
|
409
|
+
code: "credentials_auth_missing_credential_keys",
|
|
410
|
+
fix: "Return every credentialKeys entry from defineCredentialsAuth({ login }) as result.credential. Gateway persists only auth.flow complete data.credential into the connection.",
|
|
411
|
+
},
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function getPendingChallenge(ctx: FlowContext): PendingCredentialsAuthChallenge | null {
|
|
417
|
+
const value = ctx.context.get(CREDENTIALS_AUTH_CHALLENGE_CONTEXT_KEY);
|
|
418
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
419
|
+
const record = value as Record<string, unknown>;
|
|
420
|
+
if (typeof record.challengeId !== "string") return null;
|
|
421
|
+
const state =
|
|
422
|
+
record.state && typeof record.state === "object" && !Array.isArray(record.state)
|
|
423
|
+
? (record.state as Record<string, unknown>)
|
|
424
|
+
: {};
|
|
425
|
+
return {
|
|
426
|
+
challengeId: record.challengeId,
|
|
427
|
+
state,
|
|
428
|
+
...(typeof record.turnId === "string" ? { turnId: record.turnId } : {}),
|
|
429
|
+
...(typeof record.hintKey === "string" ? { hintKey: record.hintKey } : {}),
|
|
430
|
+
...(typeof record.expiresAt === "string" ? { expiresAt: record.expiresAt } : {}),
|
|
431
|
+
...(record.data && typeof record.data === "object" && !Array.isArray(record.data)
|
|
432
|
+
? { data: record.data as Record<string, unknown> }
|
|
433
|
+
: {}),
|
|
434
|
+
...(record.timing && typeof record.timing === "object" && !Array.isArray(record.timing)
|
|
435
|
+
? { timing: record.timing as AuthTurn["timing"] }
|
|
436
|
+
: {}),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function setPendingChallenge(
|
|
441
|
+
ctx: FlowContext,
|
|
442
|
+
challenge: PendingCredentialsAuthChallenge,
|
|
443
|
+
): void {
|
|
444
|
+
ctx.context.set(CREDENTIALS_AUTH_CHALLENGE_CONTEXT_KEY, challenge);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function clearPendingChallenge(ctx: FlowContext): void {
|
|
448
|
+
ctx.context.set(CREDENTIALS_AUTH_CHALLENGE_CONTEXT_KEY, null);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function pendingToChallengeRequest(
|
|
452
|
+
pending: PendingCredentialsAuthChallenge,
|
|
453
|
+
): CredentialsAuthChallengeRequest<string> {
|
|
454
|
+
return {
|
|
455
|
+
kind: "challenge",
|
|
456
|
+
challengeId: pending.challengeId,
|
|
457
|
+
state: pending.state,
|
|
458
|
+
...(pending.turnId ? { turnId: pending.turnId } : {}),
|
|
459
|
+
...(pending.hintKey ? { hintKey: pending.hintKey } : {}),
|
|
460
|
+
...(pending.expiresAt ? { expiresAt: pending.expiresAt } : {}),
|
|
461
|
+
...(pending.data ? { data: pending.data } : {}),
|
|
462
|
+
...(pending.timing ? { timing: pending.timing } : {}),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function retryTurn(
|
|
467
|
+
expectedInput: Record<string, unknown>,
|
|
468
|
+
missing: readonly string[],
|
|
469
|
+
retryTurnId: string,
|
|
470
|
+
): AuthTurn {
|
|
471
|
+
return {
|
|
472
|
+
kind: "retry",
|
|
473
|
+
turnId: retryTurnId,
|
|
474
|
+
expectedInput,
|
|
475
|
+
data: {
|
|
476
|
+
fieldErrors: Object.fromEntries(
|
|
477
|
+
missing.map((name) => [name, "Required"]),
|
|
478
|
+
),
|
|
479
|
+
fieldErrorKeys: Object.fromEntries(
|
|
480
|
+
missing.map((name) => [name, "auth.credentials.fieldRequired"]),
|
|
481
|
+
),
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function isChallengeRequest(
|
|
487
|
+
result: CredentialsAuthLoginResult<readonly string[], string>,
|
|
488
|
+
): result is CredentialsAuthChallengeRequest<string> {
|
|
489
|
+
return "kind" in result && result.kind === "challenge";
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function completeTurn<TCredentialKeys extends readonly string[]>(
|
|
493
|
+
credentialKeys: TCredentialKeys,
|
|
494
|
+
result: CredentialsAuthCompleteResult<TCredentialKeys>,
|
|
495
|
+
defaultTurnId: string,
|
|
496
|
+
): AuthTurn {
|
|
497
|
+
if (
|
|
498
|
+
!result.credential ||
|
|
499
|
+
typeof result.credential !== "object" ||
|
|
500
|
+
Array.isArray(result.credential)
|
|
501
|
+
) {
|
|
502
|
+
throw new ProviderError(
|
|
503
|
+
"Credentials auth login completed without a credential object",
|
|
504
|
+
{
|
|
505
|
+
code: "credentials_auth_missing_credential",
|
|
506
|
+
fix: "Return { credential: { ... } } from defineCredentialsAuth handlers. Gateway persists only auth.flow complete data.credential into the connection.",
|
|
507
|
+
},
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
assertCredentialKeys(
|
|
511
|
+
credentialKeys,
|
|
512
|
+
result.credential as Record<string, unknown>,
|
|
513
|
+
);
|
|
514
|
+
return {
|
|
515
|
+
kind: "complete",
|
|
516
|
+
turnId: result.turnId ?? defaultTurnId,
|
|
517
|
+
...(result.expiresAt ? { expiresAt: result.expiresAt } : {}),
|
|
518
|
+
data: {
|
|
519
|
+
...(result.data ?? {}),
|
|
520
|
+
credential: result.credential,
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function challengeTurn(
|
|
526
|
+
definition: CredentialsAuthChallengeDefinition<
|
|
527
|
+
CredentialsAuthFields,
|
|
528
|
+
readonly string[],
|
|
529
|
+
string
|
|
530
|
+
>,
|
|
531
|
+
request: CredentialsAuthChallengeRequest<string>,
|
|
532
|
+
): AuthTurn {
|
|
533
|
+
const expectedInput = definition.fields
|
|
534
|
+
? expectedInputFromFields(definition.fields)
|
|
535
|
+
: undefined;
|
|
536
|
+
return {
|
|
537
|
+
kind: expectedInput ? "form" : "pending",
|
|
538
|
+
turnId: request.turnId ?? definition.turnId ?? `credentials.${request.challengeId}`,
|
|
539
|
+
...(request.expiresAt ? { expiresAt: request.expiresAt } : {}),
|
|
540
|
+
...(request.hintKey ?? definition.hintKey
|
|
541
|
+
? { hintKey: request.hintKey ?? definition.hintKey }
|
|
542
|
+
: {}),
|
|
543
|
+
...(request.timing ?? definition.timing
|
|
544
|
+
? { timing: request.timing ?? definition.timing }
|
|
545
|
+
: {}),
|
|
546
|
+
...(expectedInput ? { expectedInput } : {}),
|
|
547
|
+
data: {
|
|
548
|
+
...(request.data ?? {}),
|
|
549
|
+
challengeId: request.challengeId,
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async function resolveAuthResult<TCredentialKeys extends readonly string[]>(
|
|
555
|
+
ctx: FlowContext,
|
|
556
|
+
credentialKeys: TCredentialKeys,
|
|
557
|
+
challenges: Record<
|
|
558
|
+
string,
|
|
559
|
+
CredentialsAuthChallengeDefinition<CredentialsAuthFields, TCredentialKeys, string>
|
|
560
|
+
>,
|
|
561
|
+
result: CredentialsAuthLoginResult<TCredentialKeys, string>,
|
|
562
|
+
completeTurnId: string,
|
|
563
|
+
): Promise<AuthTurn> {
|
|
564
|
+
if (!result || typeof result !== "object") {
|
|
565
|
+
throw new AuthError("Credentials auth login did not return a result", {
|
|
566
|
+
code: "credentials_auth_invalid_login_result",
|
|
567
|
+
fix: "Return { credential: { ... } } or credentialsAuthChallenge(...) from defineCredentialsAuth handlers.",
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
if (!isChallengeRequest(result)) {
|
|
571
|
+
clearPendingChallenge(ctx);
|
|
572
|
+
return completeTurn(credentialKeys, result, completeTurnId);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const definition = challenges[result.challengeId];
|
|
576
|
+
if (!definition) {
|
|
577
|
+
throw new ProviderError(
|
|
578
|
+
`Credentials auth requested unknown challenge "${result.challengeId}"`,
|
|
579
|
+
{
|
|
580
|
+
code: "credentials_auth_unknown_challenge",
|
|
581
|
+
fix: `Add challenges.${result.challengeId} to defineCredentialsAuth({ challenges }).`,
|
|
582
|
+
},
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
setPendingChallenge(ctx, {
|
|
586
|
+
challengeId: result.challengeId,
|
|
587
|
+
state: result.state ?? {},
|
|
588
|
+
...(result.turnId ? { turnId: result.turnId } : {}),
|
|
589
|
+
...(result.hintKey ? { hintKey: result.hintKey } : {}),
|
|
590
|
+
...(result.expiresAt ? { expiresAt: result.expiresAt } : {}),
|
|
591
|
+
...(result.data ? { data: result.data } : {}),
|
|
592
|
+
...(result.timing ? { timing: result.timing } : {}),
|
|
593
|
+
});
|
|
594
|
+
return challengeTurn(definition, result);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function continuePendingChallenge<TCredentialKeys extends readonly string[]>(
|
|
598
|
+
ctx: FlowContext,
|
|
599
|
+
credentialKeys: TCredentialKeys,
|
|
600
|
+
challenges: Record<
|
|
601
|
+
string,
|
|
602
|
+
CredentialsAuthChallengeDefinition<CredentialsAuthFields, TCredentialKeys, string>
|
|
603
|
+
>,
|
|
604
|
+
pending: PendingCredentialsAuthChallenge,
|
|
605
|
+
rawInput: Record<string, unknown> | undefined,
|
|
606
|
+
completeTurnId: string,
|
|
607
|
+
): Promise<AuthTurn> {
|
|
608
|
+
const definition = challenges[pending.challengeId];
|
|
609
|
+
if (!definition) {
|
|
610
|
+
throw new ProviderError(
|
|
611
|
+
`Credentials auth has pending unknown challenge "${pending.challengeId}"`,
|
|
612
|
+
{ code: "credentials_auth_unknown_pending_challenge" },
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
if (!definition.fields || !definition.verify) {
|
|
616
|
+
return challengeTurn(definition, pendingToChallengeRequest(pending));
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const missing = collectMissingFields(definition.fields, rawInput);
|
|
620
|
+
const expectedInput = expectedInputFromFields(definition.fields);
|
|
621
|
+
if (missing.length > 0) {
|
|
622
|
+
return retryTurn(
|
|
623
|
+
expectedInput,
|
|
624
|
+
missing,
|
|
625
|
+
definition.retryTurnId ?? `credentials.${pending.challengeId}.retry`,
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
const result = await definition.verify(
|
|
629
|
+
ctx,
|
|
630
|
+
normalizeInput(definition.fields, rawInput),
|
|
631
|
+
pending.state,
|
|
632
|
+
);
|
|
633
|
+
return await resolveAuthResult(
|
|
634
|
+
ctx,
|
|
635
|
+
credentialKeys,
|
|
636
|
+
challenges,
|
|
637
|
+
result,
|
|
638
|
+
completeTurnId,
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
async function pollPendingChallenge<TCredentialKeys extends readonly string[]>(
|
|
643
|
+
ctx: FlowContext,
|
|
644
|
+
credentialKeys: TCredentialKeys,
|
|
645
|
+
challenges: Record<
|
|
646
|
+
string,
|
|
647
|
+
CredentialsAuthChallengeDefinition<CredentialsAuthFields, TCredentialKeys, string>
|
|
648
|
+
>,
|
|
649
|
+
pending: PendingCredentialsAuthChallenge,
|
|
650
|
+
completeTurnId: string,
|
|
651
|
+
): Promise<AuthTurn> {
|
|
652
|
+
const definition = challenges[pending.challengeId];
|
|
653
|
+
if (!definition) {
|
|
654
|
+
throw new ProviderError(
|
|
655
|
+
`Credentials auth has pending unknown challenge "${pending.challengeId}"`,
|
|
656
|
+
{ code: "credentials_auth_unknown_pending_challenge" },
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
if (!definition.poll) {
|
|
660
|
+
return challengeTurn(definition, pendingToChallengeRequest(pending));
|
|
661
|
+
}
|
|
662
|
+
const result = await definition.poll(ctx, pending.state);
|
|
663
|
+
if (!result) {
|
|
664
|
+
return {
|
|
665
|
+
...challengeTurn(definition, pendingToChallengeRequest(pending)),
|
|
666
|
+
turnId:
|
|
667
|
+
definition.pendingTurnId ??
|
|
668
|
+
definition.turnId ??
|
|
669
|
+
`credentials.${pending.challengeId}.pending`,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
return await resolveAuthResult(
|
|
673
|
+
ctx,
|
|
674
|
+
credentialKeys,
|
|
675
|
+
challenges,
|
|
676
|
+
result,
|
|
677
|
+
completeTurnId,
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
export function defineCredentialsAuth<
|
|
682
|
+
TFields extends CredentialsAuthFields,
|
|
683
|
+
TCredentialKeys extends readonly [string, ...string[]],
|
|
684
|
+
TChallenges extends Record<
|
|
685
|
+
string,
|
|
686
|
+
CredentialsAuthChallengeDefinition<
|
|
687
|
+
CredentialsAuthFields,
|
|
688
|
+
TCredentialKeys,
|
|
689
|
+
keyof TChallenges & string
|
|
690
|
+
>
|
|
691
|
+
> = Record<
|
|
692
|
+
string,
|
|
693
|
+
CredentialsAuthChallengeDefinition<CredentialsAuthFields, TCredentialKeys, string>
|
|
694
|
+
>,
|
|
695
|
+
>(
|
|
696
|
+
options: DefineCredentialsAuthOptions<TFields, TCredentialKeys, TChallenges>,
|
|
697
|
+
): DefinedCredentialsAuth {
|
|
698
|
+
if (Object.keys(options.fields).length === 0) {
|
|
699
|
+
throw new ProviderError("defineCredentialsAuth requires at least one field", {
|
|
700
|
+
fix: "Pass fields such as { email: { type: \"email\" }, password: { type: \"password\" } }.",
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const expectedInput = expectedInputFromFields(options.fields);
|
|
705
|
+
const retryTurnId = options.retryTurnId ?? "credentials.retry";
|
|
706
|
+
const completeTurnId = options.completeTurnId ?? "credentials.complete";
|
|
707
|
+
const challenges = (options.challenges ?? {}) as Record<
|
|
708
|
+
string,
|
|
709
|
+
CredentialsAuthChallengeDefinition<CredentialsAuthFields, TCredentialKeys, string>
|
|
710
|
+
>;
|
|
711
|
+
|
|
712
|
+
return {
|
|
713
|
+
auth: {
|
|
714
|
+
mode: "credentials",
|
|
715
|
+
flow: {
|
|
716
|
+
start: async () => ({
|
|
717
|
+
kind: "form",
|
|
718
|
+
turnId: options.startTurnId ?? "credentials.start",
|
|
719
|
+
...(options.hintKey ? { hintKey: options.hintKey } : {}),
|
|
720
|
+
expectedInput,
|
|
721
|
+
}),
|
|
722
|
+
continue: async (ctx, rawInput) => {
|
|
723
|
+
const pending = getPendingChallenge(ctx);
|
|
724
|
+
if (pending) {
|
|
725
|
+
return await continuePendingChallenge(
|
|
726
|
+
ctx,
|
|
727
|
+
options.credentialKeys,
|
|
728
|
+
challenges,
|
|
729
|
+
pending,
|
|
730
|
+
rawInput,
|
|
731
|
+
completeTurnId,
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const missing = collectMissingFields(options.fields, rawInput);
|
|
736
|
+
if (missing.length > 0) {
|
|
737
|
+
return retryTurn(expectedInput, missing, retryTurnId);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const result = await options.login(
|
|
741
|
+
ctx,
|
|
742
|
+
normalizeInput(options.fields, rawInput),
|
|
743
|
+
);
|
|
744
|
+
return await resolveAuthResult(
|
|
745
|
+
ctx,
|
|
746
|
+
options.credentialKeys,
|
|
747
|
+
challenges,
|
|
748
|
+
result,
|
|
749
|
+
completeTurnId,
|
|
750
|
+
);
|
|
751
|
+
},
|
|
752
|
+
poll: async (ctx) => {
|
|
753
|
+
const pending = getPendingChallenge(ctx);
|
|
754
|
+
if (!pending) {
|
|
755
|
+
return {
|
|
756
|
+
kind: "pending",
|
|
757
|
+
turnId: "credentials.noPendingChallenge",
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
return await pollPendingChallenge(
|
|
761
|
+
ctx,
|
|
762
|
+
options.credentialKeys,
|
|
763
|
+
challenges,
|
|
764
|
+
pending,
|
|
765
|
+
completeTurnId,
|
|
766
|
+
);
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
},
|
|
770
|
+
credential: {
|
|
771
|
+
keys: Array.from(options.credentialKeys),
|
|
772
|
+
...(options.storesReusableSecret === undefined
|
|
773
|
+
? {}
|
|
774
|
+
: { storesReusableSecret: options.storesReusableSecret }),
|
|
775
|
+
...(options.justification ? { justification: options.justification } : {}),
|
|
776
|
+
},
|
|
777
|
+
context: {
|
|
778
|
+
keys: Array.from(
|
|
779
|
+
new Set([
|
|
780
|
+
CREDENTIALS_AUTH_CHALLENGE_CONTEXT_KEY,
|
|
781
|
+
...(options.contextKeys ?? []),
|
|
782
|
+
]),
|
|
783
|
+
),
|
|
784
|
+
},
|
|
785
|
+
};
|
|
786
|
+
}
|