@atlasauth/cli 0.7.0 → 0.8.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/CHANGELOG.md +13 -0
- package/dist/args.d.ts +28 -0
- package/dist/args.js +10 -0
- package/dist/args.js.map +1 -1
- package/dist/commands/config.d.ts +41 -1
- package/dist/commands/config.js +115 -26
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/import-better-auth.d.ts +3 -0
- package/dist/commands/import-better-auth.js +164 -0
- package/dist/commands/import-better-auth.js.map +1 -0
- package/dist/commands/import.js +7 -0
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/migrate.js +40 -1
- package/dist/commands/migrate.js.map +1 -1
- package/dist/context.d.ts +8 -0
- package/dist/context.js.map +1 -1
- package/dist/help.d.ts +1 -1
- package/dist/help.js +44 -1
- package/dist/help.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/migrate/better-auth/integration.d.ts +59 -0
- package/dist/migrate/better-auth/integration.js +81 -0
- package/dist/migrate/better-auth/integration.js.map +1 -0
- package/dist/migrate/better-auth/orchestrator.d.ts +43 -0
- package/dist/migrate/better-auth/orchestrator.js +525 -0
- package/dist/migrate/better-auth/orchestrator.js.map +1 -0
- package/dist/migrate/better-auth/output.d.ts +20 -0
- package/dist/migrate/better-auth/output.js +146 -0
- package/dist/migrate/better-auth/output.js.map +1 -0
- package/dist/migrate/better-auth/translate.d.ts +77 -0
- package/dist/migrate/better-auth/translate.js +1989 -0
- package/dist/migrate/better-auth/translate.js.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,1989 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SOCIAL_PROVIDER_MAP = void 0;
|
|
4
|
+
exports.translateBetterAuth = translateBetterAuth;
|
|
5
|
+
const upgrade_1 = require("@atlasauth/upgrade");
|
|
6
|
+
// ───────────────────────────── catalogues ─────────────────────────────
|
|
7
|
+
/** Better Auth social provider id → Atlas catalog provider key. */
|
|
8
|
+
exports.SOCIAL_PROVIDER_MAP = {
|
|
9
|
+
apple: 'apple',
|
|
10
|
+
atlassian: 'atlassian',
|
|
11
|
+
cloudflare: 'cloudflare',
|
|
12
|
+
cognito: 'cognito',
|
|
13
|
+
discord: 'discord',
|
|
14
|
+
dropbox: 'dropbox',
|
|
15
|
+
facebook: 'facebook',
|
|
16
|
+
figma: 'figma',
|
|
17
|
+
github: 'github',
|
|
18
|
+
gitlab: 'gitlab',
|
|
19
|
+
google: 'google',
|
|
20
|
+
huggingface: 'huggingface',
|
|
21
|
+
kakao: 'kakao',
|
|
22
|
+
kick: 'kick',
|
|
23
|
+
line: 'line',
|
|
24
|
+
linear: 'linear',
|
|
25
|
+
linkedin: 'linkedin',
|
|
26
|
+
microsoft: 'microsoft',
|
|
27
|
+
naver: 'naver',
|
|
28
|
+
notion: 'notion',
|
|
29
|
+
paybin: 'paybin',
|
|
30
|
+
paypal: 'paypal',
|
|
31
|
+
railway: 'railway',
|
|
32
|
+
reddit: 'reddit',
|
|
33
|
+
roblox: 'roblox',
|
|
34
|
+
salesforce: 'salesforce',
|
|
35
|
+
slack: 'slack',
|
|
36
|
+
spotify: 'spotify',
|
|
37
|
+
tiktok: 'tiktok',
|
|
38
|
+
twitch: 'twitch',
|
|
39
|
+
twitter: 'x',
|
|
40
|
+
vercel: 'vercel',
|
|
41
|
+
vk: 'vk',
|
|
42
|
+
wechat: 'wechat',
|
|
43
|
+
zoom: 'zoom',
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Social providers Better Auth ships that have no same-named Atlas catalog
|
|
47
|
+
* entry: they become custom OAuth providers with these endpoints.
|
|
48
|
+
*/
|
|
49
|
+
const CUSTOM_SOCIAL = {
|
|
50
|
+
polar: {
|
|
51
|
+
display: 'Polar',
|
|
52
|
+
protocol: 'oauth2',
|
|
53
|
+
authorize: 'https://polar.sh/oauth2/authorize',
|
|
54
|
+
token: 'https://api.polar.sh/v1/oauth2/token',
|
|
55
|
+
userinfo: 'https://api.polar.sh/v1/oauth2/userinfo',
|
|
56
|
+
scopes: ['openid', 'profile', 'email'],
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
/** Better Auth `genericOAuth` helper presets → Atlas catalog providers. */
|
|
60
|
+
const GENERIC_PRESETS = {
|
|
61
|
+
okta: 'okta',
|
|
62
|
+
auth0: 'auth0',
|
|
63
|
+
microsoftEntraId: 'microsoft',
|
|
64
|
+
slack: 'slack',
|
|
65
|
+
line: 'line',
|
|
66
|
+
};
|
|
67
|
+
/** Better Auth endpoint path → Atlas rate-limit group. */
|
|
68
|
+
const RATE_LIMIT_GROUPS = [
|
|
69
|
+
[/^\/sign-in\/(email|username|social|oauth2|anonymous|phone-number|email-otp)/, 'sign_in'],
|
|
70
|
+
[/^\/sign-in\/magic-link/, 'code_send'],
|
|
71
|
+
[/^\/sign-in/, 'sign_in'],
|
|
72
|
+
[/^\/sign-up/, 'sign_up'],
|
|
73
|
+
[/^\/(forget-password|request-password-reset|reset-password|forget-password\/email-otp|email-otp\/reset-password)/, 'password_reset'],
|
|
74
|
+
[/^\/(send-verification-email|email-otp\/send-verification-otp|phone-number\/send-otp|phone-number\/request-password-reset)/, 'code_send'],
|
|
75
|
+
[/^\/(verify-email|email-otp\/verify-email|phone-number\/verify|two-factor\/verify|two-factor\/)/, 'credential_verify'],
|
|
76
|
+
[/^\/(get-session|list-sessions|token|jwks)/, 'session_tokens'],
|
|
77
|
+
[/^\/siwe\//, 'wallet_sign_in'],
|
|
78
|
+
[/^\/(change-password|change-email|update-user|delete-user|link-social|unlink-account|passkey\/)/, 'account_changes'],
|
|
79
|
+
];
|
|
80
|
+
const CAPTCHA_PROVIDERS = {
|
|
81
|
+
'cloudflare-turnstile': 'turnstile',
|
|
82
|
+
'google-recaptcha': 'recaptcha',
|
|
83
|
+
hcaptcha: 'hcaptcha',
|
|
84
|
+
captchafox: 'captchafox',
|
|
85
|
+
'vercel-botid': 'vercel_botid',
|
|
86
|
+
};
|
|
87
|
+
/** Payment plugins by package → Atlas billing provider. */
|
|
88
|
+
const PAYMENT_PACKAGES = [
|
|
89
|
+
[/^@better-auth\/stripe$/, 'stripe'],
|
|
90
|
+
[/^@polar-sh\/better-auth/, 'polar'],
|
|
91
|
+
[/^autumn-js\/better-auth|^@useautumn\//, 'autumn'],
|
|
92
|
+
[/^@dodopayments\/better-auth/, 'dodo'],
|
|
93
|
+
[/^@creem_io\/better-auth|^creem\/better-auth/, 'creem'],
|
|
94
|
+
[/chargebee/, 'chargebee'],
|
|
95
|
+
[/^@commet\//, 'commet'],
|
|
96
|
+
];
|
|
97
|
+
/** Better Auth's built-in organization roles → Atlas system roles. */
|
|
98
|
+
const BUILTIN_ORG_ROLES = { owner: 'org:admin', admin: 'org:admin', member: 'org:member' };
|
|
99
|
+
/** Built-in organization statements → Atlas system permissions. */
|
|
100
|
+
const BUILTIN_STATEMENT_PERMS = {
|
|
101
|
+
organization: { update: 'org:sys_profile:manage', delete: 'org:sys_profile:manage' },
|
|
102
|
+
member: { create: 'org:sys_memberships:manage', update: 'org:sys_memberships:manage', delete: 'org:sys_memberships:manage' },
|
|
103
|
+
invitation: { create: 'org:sys_memberships:manage', cancel: 'org:sys_memberships:manage' },
|
|
104
|
+
team: { create: 'org:sys_memberships:manage', update: 'org:sys_memberships:manage', delete: 'org:sys_memberships:manage' },
|
|
105
|
+
ac: { read: 'org:sys_memberships:read' },
|
|
106
|
+
};
|
|
107
|
+
const DAY = 86_400;
|
|
108
|
+
const MIN_IDLE_S = 300;
|
|
109
|
+
const MAX_ABSOLUTE_S = 90 * DAY;
|
|
110
|
+
// ───────────────────────────── the builder ─────────────────────────────
|
|
111
|
+
class Builder {
|
|
112
|
+
fallbackLoc;
|
|
113
|
+
authConfig = {};
|
|
114
|
+
doc = {
|
|
115
|
+
instance: { auth_config: this.authConfig },
|
|
116
|
+
oauth_providers: [],
|
|
117
|
+
custom_oauth_providers: [],
|
|
118
|
+
permissions: [],
|
|
119
|
+
roles: [],
|
|
120
|
+
oauth_clients: [],
|
|
121
|
+
resource_servers: [],
|
|
122
|
+
billing_plans: [],
|
|
123
|
+
redirect_urls: [],
|
|
124
|
+
jwt_templates: [],
|
|
125
|
+
};
|
|
126
|
+
report = [];
|
|
127
|
+
env = new Map();
|
|
128
|
+
secrets = [];
|
|
129
|
+
providers = {};
|
|
130
|
+
roles = {};
|
|
131
|
+
plugins = [];
|
|
132
|
+
origins = [];
|
|
133
|
+
constructor(fallbackLoc) {
|
|
134
|
+
this.fallbackLoc = fallbackLoc;
|
|
135
|
+
}
|
|
136
|
+
set(path, value) {
|
|
137
|
+
const parts = path.split('.');
|
|
138
|
+
let node = this.authConfig;
|
|
139
|
+
for (const part of parts.slice(0, -1)) {
|
|
140
|
+
const next = node[part];
|
|
141
|
+
if (!next || typeof next !== 'object' || Array.isArray(next))
|
|
142
|
+
node[part] = {};
|
|
143
|
+
node = node[part];
|
|
144
|
+
}
|
|
145
|
+
node[parts[parts.length - 1]] = value;
|
|
146
|
+
}
|
|
147
|
+
get(path) {
|
|
148
|
+
let node = this.authConfig;
|
|
149
|
+
for (const part of path.split('.')) {
|
|
150
|
+
if (!node || typeof node !== 'object' || Array.isArray(node))
|
|
151
|
+
return undefined;
|
|
152
|
+
node = node[part];
|
|
153
|
+
}
|
|
154
|
+
return node;
|
|
155
|
+
}
|
|
156
|
+
add(status, area, option, note, at, atlas) {
|
|
157
|
+
const loc = !at ? this.fallbackLoc : 'kind' in at ? at.loc : at;
|
|
158
|
+
this.report.push({ status, area, option, ...(atlas ? { atlas } : {}), note, file: loc.file, line: loc.line });
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* A config string for a value: a literal stays, an env var becomes `${NAME}`.
|
|
162
|
+
* Anything else becomes a `${fallback}` placeholder and a manual entry.
|
|
163
|
+
*/
|
|
164
|
+
configString(v, purpose, fallback, opts) {
|
|
165
|
+
if (v?.kind === 'string') {
|
|
166
|
+
if (opts.secret) {
|
|
167
|
+
this.add('manual', opts.area, opts.option, `A secret is hard-coded in source. It was not copied: set ${fallback} in the environment before applying, and move the secret out of the code.`, v);
|
|
168
|
+
this.env.set(fallback, purpose);
|
|
169
|
+
return `\${${fallback}}`;
|
|
170
|
+
}
|
|
171
|
+
return v.value;
|
|
172
|
+
}
|
|
173
|
+
if (v?.kind === 'env') {
|
|
174
|
+
this.env.set(v.name, purpose);
|
|
175
|
+
return `\${${v.name}}`;
|
|
176
|
+
}
|
|
177
|
+
if (v?.kind === 'template' && v.parts.every((p) => typeof p === 'string' || p.kind === 'env')) {
|
|
178
|
+
return v.parts
|
|
179
|
+
.map((p) => {
|
|
180
|
+
if (typeof p === 'string')
|
|
181
|
+
return p;
|
|
182
|
+
if (p.kind !== 'env')
|
|
183
|
+
return '';
|
|
184
|
+
this.env.set(p.name, purpose);
|
|
185
|
+
return `\${${p.name}}`;
|
|
186
|
+
})
|
|
187
|
+
.join('');
|
|
188
|
+
}
|
|
189
|
+
this.env.set(fallback, purpose);
|
|
190
|
+
this.add('manual', opts.area, opts.option, v ? `The value cannot be read statically (${describe(v)}); set ${fallback} in the environment before applying.` : `No value found; set ${fallback} in the environment before applying.`, v);
|
|
191
|
+
return `\${${fallback}}`;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function describe(v) {
|
|
195
|
+
switch (v.kind) {
|
|
196
|
+
case 'unknown':
|
|
197
|
+
return `${v.reason}: ${v.text}`;
|
|
198
|
+
case 'call':
|
|
199
|
+
return `the result of ${v.callee}()`;
|
|
200
|
+
case 'function':
|
|
201
|
+
return 'a function';
|
|
202
|
+
case 'param':
|
|
203
|
+
return `a parameter (${[v.name, ...v.path].join('.')})`;
|
|
204
|
+
case 'external':
|
|
205
|
+
return `${v.name} from ${v.module}`;
|
|
206
|
+
default:
|
|
207
|
+
return v.kind;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const num = (v) => (v?.kind === 'number' ? v.value : undefined);
|
|
211
|
+
const bool = (v) => (v?.kind === 'boolean' ? v.value : undefined);
|
|
212
|
+
const str = (v) => (v?.kind === 'string' ? v.value : undefined);
|
|
213
|
+
const isFn = (v) => v?.kind === 'function';
|
|
214
|
+
const present = (v) => v !== undefined && v.kind !== 'undefined';
|
|
215
|
+
const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
|
|
216
|
+
/** `'15m'`, `'7d'`, `'1h'`, `'30s'` or a number (seconds) → seconds. */
|
|
217
|
+
function durationSeconds(v) {
|
|
218
|
+
if (v?.kind === 'number')
|
|
219
|
+
return v.value;
|
|
220
|
+
if (v?.kind !== 'string')
|
|
221
|
+
return undefined;
|
|
222
|
+
const m = /^\s*(\d+(?:\.\d+)?)\s*(s|sec|secs|seconds?|m|min|mins|minutes?|h|hr|hrs|hours?|d|days?|w|weeks?)?\s*$/i.exec(v.value);
|
|
223
|
+
if (!m)
|
|
224
|
+
return undefined;
|
|
225
|
+
const n = Number(m[1]);
|
|
226
|
+
const unit = (m[2] ?? 's').toLowerCase()[0];
|
|
227
|
+
const mult = unit === 'm' ? 60 : unit === 'h' ? 3600 : unit === 'd' ? DAY : unit === 'w' ? 7 * DAY : 1;
|
|
228
|
+
return Math.round(n * mult);
|
|
229
|
+
}
|
|
230
|
+
function slug(text, max = 48) {
|
|
231
|
+
const s = text
|
|
232
|
+
.toLowerCase()
|
|
233
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
234
|
+
.replace(/^_+|_+$/g, '');
|
|
235
|
+
return (s || 'custom').slice(0, max);
|
|
236
|
+
}
|
|
237
|
+
/** A Better Auth role / resource name → the `[a-z][a-z0-9_]{1,31}` segment Atlas keys use. */
|
|
238
|
+
function keySegment(text) {
|
|
239
|
+
let s = slug(text, 32);
|
|
240
|
+
if (!/^[a-z]/.test(s))
|
|
241
|
+
s = `r_${s}`;
|
|
242
|
+
if (s.startsWith('sys_'))
|
|
243
|
+
s = `app_${s}`;
|
|
244
|
+
if (s.length < 2)
|
|
245
|
+
s = `${s}_role`;
|
|
246
|
+
return s.slice(0, 32);
|
|
247
|
+
}
|
|
248
|
+
// ───────────────────────────── entry point ─────────────────────────────
|
|
249
|
+
function translateBetterAuth(analysis) {
|
|
250
|
+
const [first, ...rest] = analysis.instances;
|
|
251
|
+
const fallbackLoc = first?.loc ?? { file: '(none)', line: 0 };
|
|
252
|
+
const b = new Builder(fallbackLoc);
|
|
253
|
+
if (!first) {
|
|
254
|
+
b.add('manual', 'setup', 'betterAuth()', 'No betterAuth({...}) call was found. Point the command at the folder (or file) that creates your Better Auth instance.');
|
|
255
|
+
return finish(b, analysis, null, []);
|
|
256
|
+
}
|
|
257
|
+
for (const other of rest) {
|
|
258
|
+
b.add('manual', 'setup', 'betterAuth()', `Another betterAuth() call is here; only the one at ${first.loc.file}:${first.loc.line} was translated. Pass --entry <file> to pick one.`, other.loc);
|
|
259
|
+
}
|
|
260
|
+
for (const err of analysis.parseErrors) {
|
|
261
|
+
b.add('manual', 'setup', 'parse', `This file could not be parsed (${err.message}); options defined in it were not read.`, { file: err.file, line: 1 });
|
|
262
|
+
}
|
|
263
|
+
const opts = first.options;
|
|
264
|
+
if (opts.kind !== 'object') {
|
|
265
|
+
b.add('manual', 'setup', 'betterAuth(options)', `The options cannot be read statically (${describe(opts)}).`, opts);
|
|
266
|
+
return finish(b, analysis, first.loc, rest.map((r) => r.loc));
|
|
267
|
+
}
|
|
268
|
+
for (const spread of opts.unresolvedSpreads) {
|
|
269
|
+
b.add('manual', 'setup', '...spread', `Options spread from a value that cannot be read (${describe(spread)}); review those options by hand.`, spread);
|
|
270
|
+
}
|
|
271
|
+
translateTopLevel(b, opts);
|
|
272
|
+
translateEmailAndPassword(b, opts);
|
|
273
|
+
translateEmailVerification(b, opts);
|
|
274
|
+
translateSocialProviders(b, opts);
|
|
275
|
+
translateSession(b, opts);
|
|
276
|
+
translateAccount(b, opts);
|
|
277
|
+
translateUser(b, opts);
|
|
278
|
+
translateRateLimit(b, opts);
|
|
279
|
+
translateAdvanced(b, opts);
|
|
280
|
+
translateTrustedOrigins(b, opts);
|
|
281
|
+
translateHooks(b, opts);
|
|
282
|
+
translatePlugins(b, opts, analysis);
|
|
283
|
+
return finish(b, analysis, first.loc, rest.map((r) => r.loc));
|
|
284
|
+
}
|
|
285
|
+
function finish(b, analysis, source, others) {
|
|
286
|
+
const opts = analysis.instances[0]?.options;
|
|
287
|
+
const advanced = (0, upgrade_1.propOf)(opts, 'advanced');
|
|
288
|
+
const prefix = str((0, upgrade_1.propOf)(advanced, 'cookiePrefix')) ?? 'better-auth';
|
|
289
|
+
const secret = (0, upgrade_1.propOf)(opts, 'secret');
|
|
290
|
+
const sessionExpires = num((0, upgrade_1.propOf)((0, upgrade_1.propOf)(opts, 'session'), 'expiresIn')) ?? 7 * DAY;
|
|
291
|
+
if (b.origins.length)
|
|
292
|
+
b.doc.instance.allowed_origins = [...new Set(b.origins)];
|
|
293
|
+
return {
|
|
294
|
+
source,
|
|
295
|
+
otherInstances: others,
|
|
296
|
+
document: b.doc,
|
|
297
|
+
report: b.report,
|
|
298
|
+
env: [...b.env.entries()].map(([name, purpose]) => ({ name, for: purpose })).sort((x, y) => (x.name < y.name ? -1 : 1)),
|
|
299
|
+
secrets: b.secrets,
|
|
300
|
+
mappings: { providers: b.providers, roles: b.roles },
|
|
301
|
+
bridge: {
|
|
302
|
+
cookiePrefix: prefix,
|
|
303
|
+
sessionCookie: `${prefix}.session_token`,
|
|
304
|
+
// Better Auth reads BETTER_AUTH_SECRET (or AUTH_SECRET) when `secret` is not set.
|
|
305
|
+
secretEnv: secret?.kind === 'env' ? secret.name : secret ? null : 'BETTER_AUTH_SECRET',
|
|
306
|
+
expiresInSeconds: sessionExpires,
|
|
307
|
+
},
|
|
308
|
+
plugins: b.plugins,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
// ───────────────────────────── top level ─────────────────────────────
|
|
312
|
+
function translateTopLevel(b, opts) {
|
|
313
|
+
const appName = (0, upgrade_1.propOf)(opts, 'appName');
|
|
314
|
+
if (appName) {
|
|
315
|
+
b.add('manual', 'general', 'appName', `Set the application name and branding in the Atlas dashboard (${str(appName) ? `"${str(appName)}"` : 'value from code'}); it is shown on hosted pages and in emails.`, appName);
|
|
316
|
+
}
|
|
317
|
+
const baseURL = (0, upgrade_1.propOf)(opts, 'baseURL');
|
|
318
|
+
if (baseURL) {
|
|
319
|
+
const origin = originOf(b, baseURL, 'baseURL');
|
|
320
|
+
if (origin)
|
|
321
|
+
b.origins.push(origin);
|
|
322
|
+
b.add('mapped', 'general', 'baseURL', 'Added to the allowed origins. Atlas serves the auth endpoints itself, so there is no base path to configure.', baseURL, 'instance allowed_origins');
|
|
323
|
+
}
|
|
324
|
+
const basePath = (0, upgrade_1.propOf)(opts, 'basePath');
|
|
325
|
+
if (basePath)
|
|
326
|
+
b.add('mapped', 'general', 'basePath', 'Not needed: Atlas hosts the auth endpoints. Remove the Better Auth route handler after the cutover (the code rewrite reports it).', basePath);
|
|
327
|
+
if ((0, upgrade_1.propOf)(opts, 'secret') || (0, upgrade_1.propOf)(opts, 'secrets')) {
|
|
328
|
+
b.add('mapped', 'general', 'secret', 'Atlas signs its own sessions with per-instance keys; the Better Auth secret is only used by the session bridge during the cutover window.', (0, upgrade_1.propOf)(opts, 'secret') ?? (0, upgrade_1.propOf)(opts, 'secrets'));
|
|
329
|
+
}
|
|
330
|
+
const database = (0, upgrade_1.propOf)(opts, 'database');
|
|
331
|
+
if (database)
|
|
332
|
+
b.add('mapped', 'general', 'database', 'Atlas stores users and sessions itself. Your Better Auth database is the source of the export; keep it until the cutover is complete.', database);
|
|
333
|
+
if ((0, upgrade_1.propOf)(opts, 'secondaryStorage'))
|
|
334
|
+
b.add('mapped', 'general', 'secondaryStorage', 'Not needed: Atlas runs its own session and rate-limit storage.', (0, upgrade_1.propOf)(opts, 'secondaryStorage'));
|
|
335
|
+
const disabledPaths = (0, upgrade_1.propOf)(opts, 'disabledPaths');
|
|
336
|
+
if (disabledPaths)
|
|
337
|
+
b.add('manual', 'general', 'disabledPaths', 'Atlas has no per-path switch; turn the matching sign-in method or feature off in auth_config instead.', disabledPaths);
|
|
338
|
+
if ((0, upgrade_1.propOf)(opts, 'onAPIError'))
|
|
339
|
+
b.add('manual', 'general', 'onAPIError', 'Error pages are Atlas-hosted; route custom handling through your app (the SDK returns typed errors).', (0, upgrade_1.propOf)(opts, 'onAPIError'));
|
|
340
|
+
if ((0, upgrade_1.propOf)(opts, 'logger') || (0, upgrade_1.propOf)(opts, 'telemetry'))
|
|
341
|
+
b.add('mapped', 'general', 'logger', 'Atlas keeps an audit log and streams it (audit streams); no logger option is needed.', (0, upgrade_1.propOf)(opts, 'logger') ?? (0, upgrade_1.propOf)(opts, 'telemetry'));
|
|
342
|
+
if ((0, upgrade_1.propOf)(opts, 'experimental'))
|
|
343
|
+
b.add('manual', 'general', 'experimental', 'Experimental Better Auth flags have no Atlas counterpart; review them.', (0, upgrade_1.propOf)(opts, 'experimental'));
|
|
344
|
+
}
|
|
345
|
+
function originOf(b, v, option) {
|
|
346
|
+
if (v.kind === 'string') {
|
|
347
|
+
try {
|
|
348
|
+
return new URL(v.value).origin;
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
b.add('manual', 'general', option, `"${v.value}" is not a URL.`, v);
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (v.kind === 'env' || v.kind === 'template') {
|
|
356
|
+
return b.configString(v, 'your app origin (allowed origins)', 'APP_ORIGIN', { area: 'general', option });
|
|
357
|
+
}
|
|
358
|
+
if (v.kind === 'object') {
|
|
359
|
+
// Dynamic base URL: { allowedHosts: [...] }
|
|
360
|
+
const hosts = (0, upgrade_1.propOf)(v, 'allowedHosts');
|
|
361
|
+
const list = hosts?.kind === 'array' ? hosts.items.map(str).filter((h) => Boolean(h)) : [];
|
|
362
|
+
for (const h of list)
|
|
363
|
+
b.origins.push(h.includes('://') ? h : `https://${h}`);
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
b.add('manual', 'general', option, `Cannot read the base URL (${describe(v)}); add your app origin to allowed_origins.`, v);
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
// ───────────────────────────── email & password ─────────────────────────────
|
|
370
|
+
function translateEmailAndPassword(b, opts) {
|
|
371
|
+
const area = 'emailAndPassword';
|
|
372
|
+
const ep = (0, upgrade_1.propOf)(opts, 'emailAndPassword');
|
|
373
|
+
const enabled = bool((0, upgrade_1.propOf)(ep, 'enabled'));
|
|
374
|
+
b.set('strategies.password', enabled === true);
|
|
375
|
+
if (!ep) {
|
|
376
|
+
b.add('mapped', area, 'emailAndPassword', 'Not configured, so password sign-in is off in Atlas too.', undefined, 'auth_config.strategies.password');
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
b.add('mapped', area, 'emailAndPassword.enabled', `Password sign-in ${enabled ? 'on' : 'off'}.`, (0, upgrade_1.propOf)(ep, 'enabled') ?? ep, 'auth_config.strategies.password');
|
|
380
|
+
const requireVerification = bool((0, upgrade_1.propOf)(ep, 'requireEmailVerification'));
|
|
381
|
+
b.set('requireEmailVerification', requireVerification === true);
|
|
382
|
+
b.add('mapped', area, 'emailAndPassword.requireEmailVerification', requireVerification
|
|
383
|
+
? 'Sign-up completes only after the address is proven.'
|
|
384
|
+
: 'Off (the Better Auth default): sign-up completes at once and the address stays unverified until it is proven; Atlas reports it as unverified (email_verified false) until then.', (0, upgrade_1.propOf)(ep, 'requireEmailVerification') ?? ep, 'auth_config.requireEmailVerification');
|
|
385
|
+
const minLen = (0, upgrade_1.propOf)(ep, 'minPasswordLength');
|
|
386
|
+
if (minLen) {
|
|
387
|
+
const n = num(minLen);
|
|
388
|
+
if (n === undefined)
|
|
389
|
+
b.add('manual', area, 'emailAndPassword.minPasswordLength', `Cannot read the value (${describe(minLen)}).`, minLen);
|
|
390
|
+
else if (n < 8) {
|
|
391
|
+
b.set('passwordPolicy.minLength', 8);
|
|
392
|
+
b.add('differs', area, 'emailAndPassword.minPasswordLength', `Better Auth allows ${n}; Atlas requires at least 8. Existing shorter passwords still sign in; new ones need 8.`, minLen, 'auth_config.passwordPolicy.minLength');
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
b.set('passwordPolicy.minLength', n);
|
|
396
|
+
b.add('mapped', area, 'emailAndPassword.minPasswordLength', `${n}.`, minLen, 'auth_config.passwordPolicy.minLength');
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const maxLen = (0, upgrade_1.propOf)(ep, 'maxPasswordLength');
|
|
400
|
+
if (maxLen) {
|
|
401
|
+
const n = num(maxLen);
|
|
402
|
+
if (n === undefined)
|
|
403
|
+
b.add('manual', area, 'emailAndPassword.maxPasswordLength', `Cannot read the value (${describe(maxLen)}).`, maxLen);
|
|
404
|
+
else {
|
|
405
|
+
b.set('passwordPolicy.maxLength', clamp(n, 8, 256));
|
|
406
|
+
b.add(n > 256 ? 'differs' : 'mapped', area, 'emailAndPassword.maxPasswordLength', n > 256 ? `Atlas caps passwords at 256 characters (Better Auth: ${n}).` : `${n}.`, maxLen, 'auth_config.passwordPolicy.maxLength');
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if ((0, upgrade_1.propOf)(ep, 'disableSignUp')) {
|
|
410
|
+
if (bool((0, upgrade_1.propOf)(ep, 'disableSignUp'))) {
|
|
411
|
+
b.add('manual', area, 'emailAndPassword.disableSignUp', 'Better Auth closes password sign-up only. In Atlas, sign-up is closed for every method with signUpMode "restricted" (invitations and the allowlist still work); or keep it public and turn sign-up off per social provider (allow_sign_up).', (0, upgrade_1.propOf)(ep, 'disableSignUp'), 'auth_config.signUpMode');
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const autoSignIn = (0, upgrade_1.propOf)(ep, 'autoSignIn');
|
|
415
|
+
if (autoSignIn && bool(autoSignIn) === false) {
|
|
416
|
+
b.add('differs', area, 'emailAndPassword.autoSignIn', 'Atlas signs the user in when sign-up completes. To make people sign in again after sign-up, sign them out from your post-sign-up page.', autoSignIn);
|
|
417
|
+
}
|
|
418
|
+
const reset = (0, upgrade_1.propOf)(ep, 'sendResetPassword');
|
|
419
|
+
if (reset) {
|
|
420
|
+
b.set('accountUx.passwordReset', 'link');
|
|
421
|
+
b.add('differs', area, 'emailAndPassword.sendResetPassword', 'Atlas sends the reset email itself through your email provider, as a link (as Better Auth does). Customise the "password reset" email template instead of this function; switch accountUx.passwordReset to "code" for a one-time code.', reset, 'auth_config.accountUx.passwordReset');
|
|
422
|
+
}
|
|
423
|
+
const resetTtl = num((0, upgrade_1.propOf)(ep, 'resetPasswordTokenExpiresIn'));
|
|
424
|
+
if (resetTtl !== undefined) {
|
|
425
|
+
b.add('differs', area, 'emailAndPassword.resetPasswordTokenExpiresIn', `Reset links use the email-link lifetime (verification.magicLink.ttlMinutes), shared with sign-in links. Better Auth: ${Math.round(resetTtl / 60)} min.`, (0, upgrade_1.propOf)(ep, 'resetPasswordTokenExpiresIn'), 'auth_config.verification.magicLink.ttlMinutes');
|
|
426
|
+
if (b.get('verification.magicLink.ttlMinutes') === undefined)
|
|
427
|
+
b.set('verification.magicLink.ttlMinutes', clamp(Math.ceil(resetTtl / 60), 1, 1440));
|
|
428
|
+
}
|
|
429
|
+
// Better Auth ends a reset at the sign-in screen; Atlas defaults to signing in.
|
|
430
|
+
b.set('signInAfterReset', false);
|
|
431
|
+
b.add('mapped', area, 'emailAndPassword (reset)', 'A completed password reset ends at the sign-in screen, as in Better Auth.', ep, 'auth_config.signInAfterReset');
|
|
432
|
+
const revoke = (0, upgrade_1.propOf)(ep, 'revokeSessionsOnPasswordReset');
|
|
433
|
+
if (revoke && bool(revoke) === false) {
|
|
434
|
+
b.add('differs', area, 'emailAndPassword.revokeSessionsOnPasswordReset', 'Atlas always ends the other sessions when a password is reset.', revoke);
|
|
435
|
+
}
|
|
436
|
+
else if (revoke) {
|
|
437
|
+
b.add('mapped', area, 'emailAndPassword.revokeSessionsOnPasswordReset', 'Atlas ends the other sessions on reset.', revoke);
|
|
438
|
+
}
|
|
439
|
+
if ((0, upgrade_1.propOf)(ep, 'onPasswordReset')) {
|
|
440
|
+
b.add('manual', area, 'emailAndPassword.onPasswordReset', 'Move this logic to a webhook on the password-change event (user.updated) or an Action.', (0, upgrade_1.propOf)(ep, 'onPasswordReset'));
|
|
441
|
+
}
|
|
442
|
+
const password = (0, upgrade_1.propOf)(ep, 'password');
|
|
443
|
+
if (password && ((0, upgrade_1.propOf)(password, 'hash') || (0, upgrade_1.propOf)(password, 'verify'))) {
|
|
444
|
+
b.add('manual', area, 'emailAndPassword.password.hash', 'Custom hashing: Atlas imports scrypt (the Better Auth default), bcrypt and Argon2id hashes as they are. For any other scheme, use lazy migration (a verify endpoint Atlas calls on first sign-in).', password);
|
|
445
|
+
}
|
|
446
|
+
if ((0, upgrade_1.propOf)(ep, 'onExistingUserSignUp') || (0, upgrade_1.propOf)(ep, 'customSyntheticUser')) {
|
|
447
|
+
b.add('mapped', area, 'emailAndPassword.onExistingUserSignUp', 'Atlas never reveals whether an address is registered on sign-up (a duplicate sign-up is answered like a new one).', (0, upgrade_1.propOf)(ep, 'onExistingUserSignUp') ?? (0, upgrade_1.propOf)(ep, 'customSyntheticUser'));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function translateEmailVerification(b, opts) {
|
|
451
|
+
const area = 'emailVerification';
|
|
452
|
+
const ev = (0, upgrade_1.propOf)(opts, 'emailVerification');
|
|
453
|
+
if (!ev)
|
|
454
|
+
return;
|
|
455
|
+
if ((0, upgrade_1.propOf)(ev, 'sendVerificationEmail')) {
|
|
456
|
+
b.set('accountUx.emailVerification', 'link');
|
|
457
|
+
b.add('differs', area, 'emailVerification.sendVerificationEmail', 'Atlas sends the verification email itself (a link, as Better Auth does) through your own email provider. Customise the "verify email" template instead of this function; set accountUx.emailVerification to "code" or "both" to send a code.', (0, upgrade_1.propOf)(ev, 'sendVerificationEmail'), 'auth_config.accountUx.emailVerification');
|
|
458
|
+
}
|
|
459
|
+
const expires = num((0, upgrade_1.propOf)(ev, 'expiresIn'));
|
|
460
|
+
if (expires !== undefined) {
|
|
461
|
+
b.set('verification.magicLink.ttlMinutes', clamp(Math.ceil(expires / 60), 1, 1440));
|
|
462
|
+
b.add('mapped', area, 'emailVerification.expiresIn', `${Math.round(expires / 60)} min link lifetime.`, (0, upgrade_1.propOf)(ev, 'expiresIn'), 'auth_config.verification.magicLink.ttlMinutes');
|
|
463
|
+
}
|
|
464
|
+
const onSignUp = (0, upgrade_1.propOf)(ev, 'sendOnSignUp');
|
|
465
|
+
if (onSignUp)
|
|
466
|
+
b.add('mapped', area, 'emailVerification.sendOnSignUp', 'Atlas sends the proof during sign-up.', onSignUp);
|
|
467
|
+
const onSignIn = (0, upgrade_1.propOf)(ev, 'sendOnSignIn');
|
|
468
|
+
if (onSignIn)
|
|
469
|
+
b.add('mapped', area, 'emailVerification.sendOnSignIn', 'An unverified user signing in is asked to prove the address.', onSignIn);
|
|
470
|
+
const auto = (0, upgrade_1.propOf)(ev, 'autoSignInAfterVerification');
|
|
471
|
+
if (auto) {
|
|
472
|
+
b.add(bool(auto) ? 'mapped' : 'differs', area, 'emailVerification.autoSignInAfterVerification', bool(auto) ? 'Verifying completes the sign-up and signs the user in.' : 'Atlas signs the user in once the address is verified during sign-up.', auto);
|
|
473
|
+
}
|
|
474
|
+
for (const hook of ['beforeEmailVerification', 'afterEmailVerification', 'onEmailVerification']) {
|
|
475
|
+
const h = (0, upgrade_1.propOf)(ev, hook);
|
|
476
|
+
if (h)
|
|
477
|
+
b.add('manual', area, `emailVerification.${hook}`, 'Move this logic to a webhook on email verification (user.updated) or a post-registration Action.', h);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// ───────────────────────────── social providers ─────────────────────────────
|
|
481
|
+
function translateSocialProviders(b, opts) {
|
|
482
|
+
const sp = (0, upgrade_1.propOf)(opts, 'socialProviders');
|
|
483
|
+
const keys = [];
|
|
484
|
+
if (sp?.kind === 'object') {
|
|
485
|
+
for (const [id, cfg] of Object.entries(sp.props)) {
|
|
486
|
+
const key = translateSocialProvider(b, id, cfg);
|
|
487
|
+
if (key)
|
|
488
|
+
keys.push(key);
|
|
489
|
+
}
|
|
490
|
+
for (const spread of sp.unresolvedSpreads) {
|
|
491
|
+
b.add('manual', 'socialProviders', 'socialProviders', `Providers spread from a value that cannot be read (${describe(spread)}).`, spread);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
else if (sp) {
|
|
495
|
+
b.add('manual', 'socialProviders', 'socialProviders', `Cannot read the providers (${describe(sp)}).`, sp);
|
|
496
|
+
}
|
|
497
|
+
b.set('strategies.oauthProviders', keys);
|
|
498
|
+
}
|
|
499
|
+
function translateSocialProvider(b, id, cfg) {
|
|
500
|
+
const area = 'socialProviders';
|
|
501
|
+
const option = `socialProviders.${id}`;
|
|
502
|
+
if (cfg.kind !== 'object') {
|
|
503
|
+
b.add('manual', area, option, `Cannot read this provider's options (${describe(cfg)}).`, cfg);
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
const upper = id.replace(/[A-Z]/g, (c) => `_${c}`).toUpperCase();
|
|
507
|
+
const enabled = bool((0, upgrade_1.propOf)(cfg, 'enabled'));
|
|
508
|
+
const clientId = b.configString((0, upgrade_1.propOf)(cfg, 'clientId'), `${id} client id`, `${upper}_CLIENT_ID`, { area, option: `${option}.clientId` });
|
|
509
|
+
const scopes = (0, upgrade_1.propOf)(cfg, 'scope') ?? (0, upgrade_1.propOf)(cfg, 'scopes');
|
|
510
|
+
const scopeList = scopes?.kind === 'array' ? scopes.items.map(str).filter((s) => Boolean(s)) : undefined;
|
|
511
|
+
const disableSignUp = bool((0, upgrade_1.propOf)(cfg, 'disableSignUp')) === true || bool((0, upgrade_1.propOf)(cfg, 'disableImplicitSignUp')) === true;
|
|
512
|
+
const custom = CUSTOM_SOCIAL[id];
|
|
513
|
+
if (custom) {
|
|
514
|
+
const providerKey = `${id}_sh`.slice(0, 48);
|
|
515
|
+
b.providers[id] = providerKey;
|
|
516
|
+
const secret = b.configString((0, upgrade_1.propOf)(cfg, 'clientSecret'), `${id} client secret`, `${upper}_CLIENT_SECRET`, { area, option: `${option}.clientSecret`, secret: true });
|
|
517
|
+
b.doc.custom_oauth_providers.push({
|
|
518
|
+
provider_key: providerKey,
|
|
519
|
+
display_name: custom.display,
|
|
520
|
+
protocol: custom.protocol,
|
|
521
|
+
authorization_endpoint: custom.authorize,
|
|
522
|
+
token_endpoint: custom.token,
|
|
523
|
+
userinfo_endpoint: custom.userinfo,
|
|
524
|
+
scopes: scopeList ?? custom.scopes,
|
|
525
|
+
client_id: clientId,
|
|
526
|
+
client_secret: secret,
|
|
527
|
+
enabled: enabled !== false,
|
|
528
|
+
allow_sign_in: true,
|
|
529
|
+
allow_sign_up: !disableSignUp,
|
|
530
|
+
});
|
|
531
|
+
b.add('differs', area, option, `Atlas has no built-in "${id}" provider with these endpoints; it becomes the custom OAuth provider "${providerKey}". Register its redirect_uri (shown on the provider) in your ${custom.display} app.`, cfg, `custom_oauth_providers ${providerKey}`);
|
|
532
|
+
return providerKey;
|
|
533
|
+
}
|
|
534
|
+
const atlasKey = exports.SOCIAL_PROVIDER_MAP[id];
|
|
535
|
+
if (!atlasKey) {
|
|
536
|
+
b.add('manual', area, option, `"${id}" is not a Better Auth provider Atlas knows. Configure it as a custom OAuth provider (custom_oauth_providers) with its endpoints.`, cfg);
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
b.providers[id] = atlasKey;
|
|
540
|
+
const entry = {
|
|
541
|
+
provider: atlasKey,
|
|
542
|
+
enabled: enabled !== false,
|
|
543
|
+
allow_sign_in: true,
|
|
544
|
+
allow_sign_up: !disableSignUp,
|
|
545
|
+
client_id: clientId,
|
|
546
|
+
};
|
|
547
|
+
if (atlasKey === 'apple') {
|
|
548
|
+
entry.values = {
|
|
549
|
+
team_id: '${APPLE_TEAM_ID}',
|
|
550
|
+
key_id: '${APPLE_KEY_ID}',
|
|
551
|
+
private_key: '${APPLE_PRIVATE_KEY}',
|
|
552
|
+
};
|
|
553
|
+
b.env.set('APPLE_TEAM_ID', 'Sign in with Apple team id');
|
|
554
|
+
b.env.set('APPLE_KEY_ID', 'Sign in with Apple key id');
|
|
555
|
+
b.env.set('APPLE_PRIVATE_KEY', 'Sign in with Apple .p8 key contents');
|
|
556
|
+
b.add('differs', area, `${option}.clientSecret`, 'Atlas builds the Apple client secret itself from your team id, key id and .p8 key (no pre-generated JWT); set APPLE_TEAM_ID, APPLE_KEY_ID and APPLE_PRIVATE_KEY. Keep the same Services ID so Apple user ids stay the same.', (0, upgrade_1.propOf)(cfg, 'clientSecret') ?? cfg, `oauth_providers ${atlasKey}`);
|
|
557
|
+
if ((0, upgrade_1.propOf)(cfg, 'appBundleIdentifier'))
|
|
558
|
+
b.add('manual', area, `${option}.appBundleIdentifier`, 'Add the iOS app to auth_config.nativeApps (appleAppIds) for native Sign in with Apple.', (0, upgrade_1.propOf)(cfg, 'appBundleIdentifier'), 'auth_config.nativeApps.appleAppIds');
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
entry.client_secret = b.configString((0, upgrade_1.propOf)(cfg, 'clientSecret'), `${id} client secret`, `${upper}_CLIENT_SECRET`, { area, option: `${option}.clientSecret`, secret: true });
|
|
562
|
+
}
|
|
563
|
+
if (scopeList?.length)
|
|
564
|
+
entry.scopes = scopeList;
|
|
565
|
+
const values = entry.values ?? {};
|
|
566
|
+
if (atlasKey === 'microsoft') {
|
|
567
|
+
const tenant = (0, upgrade_1.propOf)(cfg, 'tenantId');
|
|
568
|
+
if (tenant) {
|
|
569
|
+
values.tenant = b.configString(tenant, 'Microsoft tenant id', 'MICROSOFT_TENANT_ID', { area, option: `${option}.tenantId` });
|
|
570
|
+
b.add('mapped', area, `${option}.tenantId`, 'Tenant restriction kept.', tenant, `oauth_providers microsoft (tenant)`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (atlasKey === 'cognito') {
|
|
574
|
+
const domain = (0, upgrade_1.propOf)(cfg, 'domain');
|
|
575
|
+
if (domain)
|
|
576
|
+
values.domain = b.configString(domain, 'Cognito domain', 'COGNITO_DOMAIN', { area, option: `${option}.domain` });
|
|
577
|
+
else
|
|
578
|
+
b.add('manual', area, option, 'Set the Cognito user pool domain on the provider.', cfg);
|
|
579
|
+
}
|
|
580
|
+
if (atlasKey === 'google') {
|
|
581
|
+
const hd = (0, upgrade_1.propOf)(cfg, 'hd') ?? (0, upgrade_1.propOf)((0, upgrade_1.propOf)(cfg, 'authorizationParams') ?? (0, upgrade_1.propOf)(cfg, 'params'), 'hd');
|
|
582
|
+
if (hd)
|
|
583
|
+
values.hosted_domain = b.configString(hd, 'Google Workspace domain', 'GOOGLE_HOSTED_DOMAIN', { area, option: `${option}.hd` });
|
|
584
|
+
if ((0, upgrade_1.propOf)(cfg, 'accessType') || (0, upgrade_1.propOf)(cfg, 'prompt')) {
|
|
585
|
+
b.add('mapped', area, `${option}.accessType`, 'Atlas asks Google for offline access (refresh tokens) by default.', (0, upgrade_1.propOf)(cfg, 'accessType') ?? (0, upgrade_1.propOf)(cfg, 'prompt'));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (Object.keys(values).length)
|
|
589
|
+
entry.values = values;
|
|
590
|
+
b.doc.oauth_providers.push(entry);
|
|
591
|
+
b.add('mapped', area, option, `Keep the same OAuth app so imported accounts relink by provider user id. Add Atlas's callback URL (redirect_uri on the provider) to the app's allowed redirect URLs.`, cfg, `oauth_providers ${atlasKey}`);
|
|
592
|
+
if (disableSignUp)
|
|
593
|
+
b.add('mapped', area, `${option}.disableSignUp`, 'Sign-up through this provider is off (sign-in still works).', (0, upgrade_1.propOf)(cfg, 'disableSignUp') ?? (0, upgrade_1.propOf)(cfg, 'disableImplicitSignUp'), `oauth_providers ${atlasKey} allow_sign_up`);
|
|
594
|
+
const redirect = (0, upgrade_1.propOf)(cfg, 'redirectURI');
|
|
595
|
+
if (redirect)
|
|
596
|
+
b.add('manual', area, `${option}.redirectURI`, 'Atlas receives the callback on its own domain. To keep this exact redirect URI, set a custom redirect URI on the provider in Atlas (on a domain you verify) that forwards to Atlas.', redirect);
|
|
597
|
+
for (const fnKey of ['mapProfileToUser', 'getUserInfo', 'verifyIdToken', 'refreshAccessToken']) {
|
|
598
|
+
const fn = (0, upgrade_1.propOf)(cfg, fnKey);
|
|
599
|
+
if (fn)
|
|
600
|
+
b.add('manual', area, `${option}.${fnKey}`, fnKey === 'mapProfileToUser' ? 'Built-in providers use Atlas\'s profile mapping; move extra profile fields into user metadata with a post-login Action.' : 'Atlas runs this step itself for built-in providers; remove the override.', fn);
|
|
601
|
+
}
|
|
602
|
+
if ((0, upgrade_1.propOf)(cfg, 'overrideUserInfoOnSignIn'))
|
|
603
|
+
b.add('differs', area, `${option}.overrideUserInfoOnSignIn`, 'Atlas fills empty profile fields from the provider; to overwrite on every sign-in, use a post-login Action.', (0, upgrade_1.propOf)(cfg, 'overrideUserInfoOnSignIn'));
|
|
604
|
+
return atlasKey;
|
|
605
|
+
}
|
|
606
|
+
// ───────────────────────────── session ─────────────────────────────
|
|
607
|
+
function translateSession(b, opts) {
|
|
608
|
+
const area = 'session';
|
|
609
|
+
const s = (0, upgrade_1.propOf)(opts, 'session');
|
|
610
|
+
const expires = num((0, upgrade_1.propOf)(s, 'expiresIn')) ?? 7 * DAY;
|
|
611
|
+
const noRefresh = bool((0, upgrade_1.propOf)(s, 'disableSessionRefresh')) === true;
|
|
612
|
+
const loc = (0, upgrade_1.propOf)(s, 'expiresIn') ?? s;
|
|
613
|
+
if (noRefresh) {
|
|
614
|
+
const abs = clamp(expires, MIN_IDLE_S, MAX_ABSOLUTE_S);
|
|
615
|
+
b.set('session.absoluteMs', abs * 1000);
|
|
616
|
+
b.set('session.idleMs', abs * 1000);
|
|
617
|
+
b.set('session.idleTimeout', false);
|
|
618
|
+
b.add(abs === expires ? 'mapped' : 'differs', area, 'session.expiresIn + disableSessionRefresh', `A fixed lifetime of ${humanDuration(abs)} that activity does not extend${abs !== expires ? ` (Better Auth: ${humanDuration(expires)}; Atlas allows 5 minutes to 90 days)` : ''}.`, loc, 'auth_config.session.absoluteMs');
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
const idle = clamp(expires, MIN_IDLE_S, MAX_ABSOLUTE_S);
|
|
622
|
+
b.set('session.idleMs', idle * 1000);
|
|
623
|
+
b.set('session.idleTimeout', true);
|
|
624
|
+
b.set('session.absoluteMs', MAX_ABSOLUTE_S * 1000);
|
|
625
|
+
b.add('differs', area, 'session.expiresIn', `Better Auth extends a session by ${humanDuration(expires)} whenever it is used and never caps it. Atlas keeps the same inactivity window (idleMs ${humanDuration(idle)}) and adds an absolute cap, set to the maximum of 90 days; lower absoluteMs for a stricter policy.`, loc ?? undefined, 'auth_config.session.idleMs / absoluteMs');
|
|
626
|
+
}
|
|
627
|
+
const updateAge = (0, upgrade_1.propOf)(s, 'updateAge');
|
|
628
|
+
if (updateAge)
|
|
629
|
+
b.add('mapped', area, 'session.updateAge', 'Atlas extends the inactivity window on every authenticated request; no refresh interval is needed.', updateAge);
|
|
630
|
+
const freshAge = (0, upgrade_1.propOf)(s, 'freshAge');
|
|
631
|
+
if (freshAge) {
|
|
632
|
+
const n = num(freshAge);
|
|
633
|
+
if (n === 0) {
|
|
634
|
+
b.set('stepUp.enabled', false);
|
|
635
|
+
b.add('mapped', area, 'session.freshAge', 'Freshness checks off: step-up re-authentication stays off.', freshAge, 'auth_config.stepUp.enabled');
|
|
636
|
+
}
|
|
637
|
+
else if (n !== undefined) {
|
|
638
|
+
const win = clamp(n, 30, DAY);
|
|
639
|
+
b.set('stepUp.enabled', true);
|
|
640
|
+
b.set('stepUp.windowMs', win * 1000);
|
|
641
|
+
b.add(win === n ? 'mapped' : 'differs', area, 'session.freshAge', `Sensitive actions need a sign-in within ${humanDuration(win)} (Atlas step-up)${win !== n ? `; Atlas allows 30 seconds to 1 day (Better Auth: ${humanDuration(n)})` : ''}.`, freshAge, 'auth_config.stepUp');
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const cache = (0, upgrade_1.propOf)(s, 'cookieCache');
|
|
645
|
+
if (cache) {
|
|
646
|
+
const maxAge = num((0, upgrade_1.propOf)(cache, 'maxAge'));
|
|
647
|
+
const ttl = clamp(maxAge ?? 300, 30, 900);
|
|
648
|
+
b.set('session.jwtTtlSeconds', ttl);
|
|
649
|
+
b.add('differs', area, 'session.cookieCache', `Atlas's short-lived session token plays the role of the cookie cache: it is verified without a database read and refreshed every ${ttl}s (Atlas allows 30-900 s)${(0, upgrade_1.propOf)(cache, 'strategy') ? '; the token is always a signed JWT' : ''}.`, cache, 'auth_config.session.jwtTtlSeconds');
|
|
650
|
+
}
|
|
651
|
+
for (const k of ['storeSessionInDatabase', 'preserveSessionInDatabase', 'deferSessionRefresh']) {
|
|
652
|
+
if ((0, upgrade_1.propOf)(s, k))
|
|
653
|
+
b.add('mapped', area, `session.${k}`, 'Atlas stores sessions itself; nothing to configure.', (0, upgrade_1.propOf)(s, k));
|
|
654
|
+
}
|
|
655
|
+
const fields = (0, upgrade_1.propOf)(s, 'additionalFields');
|
|
656
|
+
if (fields)
|
|
657
|
+
b.add('manual', area, 'session.additionalFields', 'Store per-session data with the session custom data API (session.customData) and set it from a post-login Action.', fields, 'auth_config.session.customData');
|
|
658
|
+
}
|
|
659
|
+
function humanDuration(seconds) {
|
|
660
|
+
if (seconds % DAY === 0)
|
|
661
|
+
return `${seconds / DAY} day${seconds === DAY ? '' : 's'}`;
|
|
662
|
+
if (seconds % 3600 === 0)
|
|
663
|
+
return `${seconds / 3600} h`;
|
|
664
|
+
if (seconds % 60 === 0)
|
|
665
|
+
return `${seconds / 60} min`;
|
|
666
|
+
return `${seconds} s`;
|
|
667
|
+
}
|
|
668
|
+
// ───────────────────────────── account / user ─────────────────────────────
|
|
669
|
+
function translateAccount(b, opts) {
|
|
670
|
+
const area = 'account';
|
|
671
|
+
const acc = (0, upgrade_1.propOf)(opts, 'account');
|
|
672
|
+
const linking = (0, upgrade_1.propOf)(acc, 'accountLinking');
|
|
673
|
+
if (linking) {
|
|
674
|
+
const enabled = bool((0, upgrade_1.propOf)(linking, 'enabled'));
|
|
675
|
+
const implicitOff = bool((0, upgrade_1.propOf)(linking, 'disableImplicitLinking')) === true;
|
|
676
|
+
if (enabled === false || implicitOff) {
|
|
677
|
+
b.set('accountLinking', 'block');
|
|
678
|
+
b.add('mapped', area, enabled === false ? 'account.accountLinking.enabled' : 'account.accountLinking.disableImplicitLinking', 'A social sign-in whose email belongs to an existing account is refused and names the existing method; users link accounts from their profile.', (0, upgrade_1.propOf)(linking, enabled === false ? 'enabled' : 'disableImplicitLinking'), 'auth_config.accountLinking');
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
b.set('accountLinking', 'auto');
|
|
682
|
+
b.add('mapped', area, 'account.accountLinking', 'A social sign-in with a verified email that matches an existing account links to it.', linking, 'auth_config.accountLinking');
|
|
683
|
+
}
|
|
684
|
+
const trusted = (0, upgrade_1.propOf)(linking, 'trustedProviders');
|
|
685
|
+
if (trusted?.kind === 'array') {
|
|
686
|
+
const trust = {};
|
|
687
|
+
for (const item of trusted.items) {
|
|
688
|
+
const id = str(item);
|
|
689
|
+
if (!id)
|
|
690
|
+
continue;
|
|
691
|
+
if (id === 'email-password' || id === 'credential')
|
|
692
|
+
continue;
|
|
693
|
+
const key = exports.SOCIAL_PROVIDER_MAP[id] ?? b.providers[id] ?? slug(id);
|
|
694
|
+
trust[key] = 'always';
|
|
695
|
+
}
|
|
696
|
+
b.set('accountLinkingTrust', trust);
|
|
697
|
+
b.add('mapped', area, 'account.accountLinking.trustedProviders', `These providers link even when they do not assert a verified email: ${Object.keys(trust).join(', ') || '(none)'}.`, trusted, 'auth_config.accountLinkingTrust');
|
|
698
|
+
}
|
|
699
|
+
else if (trusted) {
|
|
700
|
+
b.add('manual', area, 'account.accountLinking.trustedProviders', `Cannot read the list (${describe(trusted)}); set accountLinkingTrust by hand.`, trusted);
|
|
701
|
+
}
|
|
702
|
+
const requireLocal = (0, upgrade_1.propOf)(linking, 'requireLocalEmailVerified');
|
|
703
|
+
if (requireLocal)
|
|
704
|
+
b.add('mapped', area, 'account.accountLinking.requireLocalEmailVerified', 'Atlas only auto-links to an account whose own address is proven.', requireLocal);
|
|
705
|
+
const differentEmails = (0, upgrade_1.propOf)(linking, 'allowDifferentEmails');
|
|
706
|
+
if (differentEmails)
|
|
707
|
+
b.add(bool(differentEmails) ? 'mapped' : 'differs', area, 'account.accountLinking.allowDifferentEmails', 'A signed-in user can connect a provider account with a different email from their profile; automatic linking always needs a matching email.', differentEmails);
|
|
708
|
+
const unlinkAll = (0, upgrade_1.propOf)(linking, 'allowUnlinkingAll');
|
|
709
|
+
if (unlinkAll)
|
|
710
|
+
b.add('differs', area, 'account.accountLinking.allowUnlinkingAll', 'Atlas never lets a user remove their last way to sign in.', unlinkAll);
|
|
711
|
+
const updateOnLink = (0, upgrade_1.propOf)(linking, 'updateUserInfoOnLink');
|
|
712
|
+
if (updateOnLink)
|
|
713
|
+
b.add('differs', area, 'account.accountLinking.updateUserInfoOnLink', 'Atlas fills empty profile fields on link; use a post-login Action to overwrite.', updateOnLink);
|
|
714
|
+
}
|
|
715
|
+
if ((0, upgrade_1.propOf)(acc, 'encryptOAuthTokens'))
|
|
716
|
+
b.add('mapped', area, 'account.encryptOAuthTokens', 'Atlas always encrypts stored provider tokens.', (0, upgrade_1.propOf)(acc, 'encryptOAuthTokens'));
|
|
717
|
+
if ((0, upgrade_1.propOf)(acc, 'updateAccountOnSignIn'))
|
|
718
|
+
b.add('mapped', area, 'account.updateAccountOnSignIn', 'Provider tokens are refreshed on every sign-in.', (0, upgrade_1.propOf)(acc, 'updateAccountOnSignIn'));
|
|
719
|
+
for (const k of ['skipStateCookieCheck', 'storeStateStrategy', 'storeAccountCookie']) {
|
|
720
|
+
if ((0, upgrade_1.propOf)(acc, k))
|
|
721
|
+
b.add('mapped', area, `account.${k}`, 'Atlas runs the OAuth state handling itself.', (0, upgrade_1.propOf)(acc, k));
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
function translateUser(b, opts) {
|
|
725
|
+
const area = 'user';
|
|
726
|
+
const user = (0, upgrade_1.propOf)(opts, 'user');
|
|
727
|
+
if (!user)
|
|
728
|
+
return;
|
|
729
|
+
const fields = (0, upgrade_1.propOf)(user, 'additionalFields');
|
|
730
|
+
if (fields?.kind === 'object') {
|
|
731
|
+
const traits = [];
|
|
732
|
+
for (const [key, def] of Object.entries(fields.props)) {
|
|
733
|
+
const type = str((0, upgrade_1.propOf)(def, 'type'));
|
|
734
|
+
const traitKey = /^[a-zA-Z][\w.-]{0,63}$/.test(key) ? key : null;
|
|
735
|
+
if (!traitKey) {
|
|
736
|
+
b.add('manual', area, `user.additionalFields.${key}`, 'The field name is not a valid metadata key; rename it in the import.', def);
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
const trait = { key: traitKey };
|
|
740
|
+
if (type === 'string' || type === 'number' || type === 'boolean')
|
|
741
|
+
trait.type = type;
|
|
742
|
+
else if (def.kind === 'object' && (0, upgrade_1.propOf)(def, 'type')?.kind === 'array') {
|
|
743
|
+
const values = (0, upgrade_1.propOf)(def, 'type').items.map(str).filter((s) => Boolean(s));
|
|
744
|
+
trait.type = 'enum';
|
|
745
|
+
trait.enum = values;
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
b.add('manual', area, `user.additionalFields.${key}`, `Type "${type ?? describe((0, upgrade_1.propOf)(def, 'type') ?? def)}" has no typed trait; the value is still imported into public metadata.`, def);
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
if (bool((0, upgrade_1.propOf)(def, 'required')))
|
|
752
|
+
trait.required = true;
|
|
753
|
+
trait.userEditable = bool((0, upgrade_1.propOf)(def, 'input')) !== false;
|
|
754
|
+
traits.push(trait);
|
|
755
|
+
b.add('mapped', area, `user.additionalFields.${key}`, `Typed trait "${traitKey}" on the user's public metadata${trait.userEditable ? '' : ' (not editable by the user)'}.`, def, 'auth_config.identityTraits');
|
|
756
|
+
}
|
|
757
|
+
if (traits.length)
|
|
758
|
+
b.set('identityTraits.traits', traits);
|
|
759
|
+
}
|
|
760
|
+
else if (fields) {
|
|
761
|
+
b.add('manual', area, 'user.additionalFields', `Cannot read the fields (${describe(fields)}); they are imported into public metadata.`, fields);
|
|
762
|
+
}
|
|
763
|
+
if ((0, upgrade_1.propOf)(user, 'changeEmail'))
|
|
764
|
+
b.add('mapped', area, 'user.changeEmail', 'Users change their email from the account page; the new address is proven first.', (0, upgrade_1.propOf)(user, 'changeEmail'));
|
|
765
|
+
const del = (0, upgrade_1.propOf)(user, 'deleteUser');
|
|
766
|
+
if (del) {
|
|
767
|
+
b.add('mapped', area, 'user.deleteUser', 'Users can delete their account from the account page; deletion has a grace period (compliance.retention).', del);
|
|
768
|
+
for (const k of ['sendDeleteAccountVerification', 'beforeDelete', 'afterDelete']) {
|
|
769
|
+
if ((0, upgrade_1.propOf)(del, k))
|
|
770
|
+
b.add('manual', area, `user.deleteUser.${k}`, 'Move this to a webhook on user.deleted.', (0, upgrade_1.propOf)(del, k));
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
if ((0, upgrade_1.propOf)(user, 'modelName') || (0, upgrade_1.propOf)(user, 'fields'))
|
|
774
|
+
b.add('mapped', area, 'user.modelName', 'Custom table and column names are read by the exporter from your Better Auth config.', (0, upgrade_1.propOf)(user, 'modelName') ?? (0, upgrade_1.propOf)(user, 'fields'));
|
|
775
|
+
if ((0, upgrade_1.propOf)(user, 'validateUserInfo'))
|
|
776
|
+
b.add('manual', area, 'user.validateUserInfo', 'Move this check to a pre-user-registration Action (and post-login for sign-ins).', (0, upgrade_1.propOf)(user, 'validateUserInfo'));
|
|
777
|
+
}
|
|
778
|
+
// ───────────────────────────── rate limits ─────────────────────────────
|
|
779
|
+
function translateRateLimit(b, opts) {
|
|
780
|
+
const area = 'rateLimit';
|
|
781
|
+
const rl = (0, upgrade_1.propOf)(opts, 'rateLimit');
|
|
782
|
+
if (!rl)
|
|
783
|
+
return;
|
|
784
|
+
if (bool((0, upgrade_1.propOf)(rl, 'enabled')) === false) {
|
|
785
|
+
b.add('differs', area, 'rateLimit.enabled', 'Atlas always rate-limits its auth endpoints.', (0, upgrade_1.propOf)(rl, 'enabled'));
|
|
786
|
+
}
|
|
787
|
+
if ((0, upgrade_1.propOf)(rl, 'window') || (0, upgrade_1.propOf)(rl, 'max')) {
|
|
788
|
+
b.add('differs', area, 'rateLimit.window/max', `Atlas applies per-endpoint-group defaults instead of one global rule (Better Auth: ${num((0, upgrade_1.propOf)(rl, 'max')) ?? 100} per ${num((0, upgrade_1.propOf)(rl, 'window')) ?? 10}s).`, (0, upgrade_1.propOf)(rl, 'window') ?? (0, upgrade_1.propOf)(rl, 'max'));
|
|
789
|
+
}
|
|
790
|
+
const custom = (0, upgrade_1.propOf)(rl, 'customRules');
|
|
791
|
+
const rules = new Map();
|
|
792
|
+
if (custom?.kind === 'object') {
|
|
793
|
+
for (const [path, rule] of Object.entries(custom.props)) {
|
|
794
|
+
const option = `rateLimit.customRules["${path}"]`;
|
|
795
|
+
const group = RATE_LIMIT_GROUPS.find(([re]) => re.test(path))?.[1];
|
|
796
|
+
if (!group) {
|
|
797
|
+
b.add('manual', area, option, 'No matching Atlas endpoint group; Atlas applies its default for this endpoint.', rule);
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (rule.kind === 'boolean' && rule.value === false) {
|
|
801
|
+
b.add('differs', area, option, `Atlas cannot switch off rate limiting for ${group}; the default applies.`, rule);
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
const windowS = num((0, upgrade_1.propOf)(rule, 'window'));
|
|
805
|
+
const max = num((0, upgrade_1.propOf)(rule, 'max'));
|
|
806
|
+
if (windowS === undefined || max === undefined) {
|
|
807
|
+
b.add('manual', area, option, `Cannot read the rule (${describe(rule)}); set a rule for the "${group}" group by hand.`, rule, 'auth_config.rateLimit.rules');
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
const next = { windowSeconds: clamp(Math.round(windowS), 1, 86_400), max: clamp(Math.round(max), 1, 1_000_000), key: 'ip' };
|
|
811
|
+
const prev = rules.get(group);
|
|
812
|
+
// Several Better Auth paths can share one Atlas group: keep the strictest rate.
|
|
813
|
+
if (!prev || next.max / next.windowSeconds < prev.max / prev.windowSeconds)
|
|
814
|
+
rules.set(group, next);
|
|
815
|
+
b.add(prev ? 'differs' : 'mapped', area, option, prev ? `Shares the "${group}" group with another rule; the stricter one is kept.` : `"${group}" group: ${next.max} per ${next.windowSeconds}s per IP.`, rule, 'auth_config.rateLimit.rules');
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
else if (custom) {
|
|
819
|
+
b.add('manual', area, 'rateLimit.customRules', `Cannot read the rules (${describe(custom)}).`, custom);
|
|
820
|
+
}
|
|
821
|
+
if (rules.size) {
|
|
822
|
+
b.set('rateLimit.rules', [...rules.entries()].map(([group, r]) => ({ group, windowSeconds: r.windowSeconds, max: r.max, key: r.key })));
|
|
823
|
+
}
|
|
824
|
+
if ((0, upgrade_1.propOf)(rl, 'storage') || (0, upgrade_1.propOf)(rl, 'customStorage'))
|
|
825
|
+
b.add('mapped', area, 'rateLimit.storage', 'Atlas keeps its own rate-limit counters.', (0, upgrade_1.propOf)(rl, 'storage') ?? (0, upgrade_1.propOf)(rl, 'customStorage'));
|
|
826
|
+
}
|
|
827
|
+
// ───────────────────────────── advanced / origins ─────────────────────────────
|
|
828
|
+
function translateAdvanced(b, opts) {
|
|
829
|
+
const area = 'advanced';
|
|
830
|
+
const adv = (0, upgrade_1.propOf)(opts, 'advanced');
|
|
831
|
+
const cross = (0, upgrade_1.propOf)(adv, 'crossSubDomainCookies');
|
|
832
|
+
if (cross) {
|
|
833
|
+
const enabled = bool((0, upgrade_1.propOf)(cross, 'enabled'));
|
|
834
|
+
b.set('session.crossSubdomainCookies', enabled === true);
|
|
835
|
+
b.add('mapped', area, 'advanced.crossSubDomainCookies.enabled', `Session cookie ${enabled ? 'shared across' : 'not shared across'} subdomains.`, (0, upgrade_1.propOf)(cross, 'enabled') ?? cross, 'auth_config.session.crossSubdomainCookies');
|
|
836
|
+
if ((0, upgrade_1.propOf)(cross, 'domain'))
|
|
837
|
+
b.add('manual', area, 'advanced.crossSubDomainCookies.domain', 'The cookie domain follows your Atlas custom domain: add the custom domain (e.g. auth.your-domain.com) in the dashboard.', (0, upgrade_1.propOf)(cross, 'domain'));
|
|
838
|
+
}
|
|
839
|
+
else {
|
|
840
|
+
// Better Auth scopes its cookie to one host by default.
|
|
841
|
+
b.set('session.crossSubdomainCookies', false);
|
|
842
|
+
}
|
|
843
|
+
if (!adv)
|
|
844
|
+
return;
|
|
845
|
+
const prefix = (0, upgrade_1.propOf)(adv, 'cookiePrefix');
|
|
846
|
+
if (prefix)
|
|
847
|
+
b.add('differs', area, 'advanced.cookiePrefix', 'Atlas cookie names are fixed. The session bridge reads the old cookie under this prefix during the cutover window.', prefix);
|
|
848
|
+
for (const k of ['cookies', 'defaultCookieAttributes', 'useSecureCookies']) {
|
|
849
|
+
if ((0, upgrade_1.propOf)(adv, k))
|
|
850
|
+
b.add('differs', area, `advanced.${k}`, 'Atlas sets secure, HttpOnly, SameSite cookies itself; cookie names and attributes are not configurable.', (0, upgrade_1.propOf)(adv, k));
|
|
851
|
+
}
|
|
852
|
+
for (const k of ['disableCSRFCheck', 'disableOriginCheck']) {
|
|
853
|
+
if (bool((0, upgrade_1.propOf)(adv, k)))
|
|
854
|
+
b.add('differs', area, `advanced.${k}`, 'Atlas always checks the request origin against allowed_origins; add every origin that calls it.', (0, upgrade_1.propOf)(adv, k), 'instance allowed_origins');
|
|
855
|
+
}
|
|
856
|
+
const ip = (0, upgrade_1.propOf)(adv, 'ipAddress');
|
|
857
|
+
if (ip)
|
|
858
|
+
b.add('manual', area, 'advanced.ipAddress', 'Atlas reads the client IP from its edge. If requests reach Atlas through your own proxy (the proxy integration), configure the trusted proxy secret in the dashboard.', ip);
|
|
859
|
+
if ((0, upgrade_1.propOf)(adv, 'trustedProxyHeaders'))
|
|
860
|
+
b.add('manual', area, 'advanced.trustedProxyHeaders', 'Configure the Atlas proxy integration for requests forwarded through your server.', (0, upgrade_1.propOf)(adv, 'trustedProxyHeaders'));
|
|
861
|
+
const db = (0, upgrade_1.propOf)(adv, 'database');
|
|
862
|
+
if (db)
|
|
863
|
+
b.add('mapped', area, 'advanced.database', 'Atlas issues its own ids; every user keeps its Better Auth id as external_id.', db);
|
|
864
|
+
if ((0, upgrade_1.propOf)(adv, 'backgroundTasks'))
|
|
865
|
+
b.add('mapped', area, 'advanced.backgroundTasks', 'Not needed: Atlas runs deferred work itself.', (0, upgrade_1.propOf)(adv, 'backgroundTasks'));
|
|
866
|
+
}
|
|
867
|
+
function translateTrustedOrigins(b, opts) {
|
|
868
|
+
const area = 'trustedOrigins';
|
|
869
|
+
const to = (0, upgrade_1.propOf)(opts, 'trustedOrigins');
|
|
870
|
+
if (!to)
|
|
871
|
+
return;
|
|
872
|
+
if (to.kind === 'function') {
|
|
873
|
+
b.add('manual', area, 'trustedOrigins', 'A function decides the origins at runtime; list the origins in allowed_origins by hand.', to, 'instance allowed_origins');
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
if (to.kind !== 'array') {
|
|
877
|
+
b.add('manual', area, 'trustedOrigins', `Cannot read the list (${describe(to)}).`, to);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
for (const item of to.items) {
|
|
881
|
+
if (item.kind === 'string') {
|
|
882
|
+
const v = item.value.trim();
|
|
883
|
+
if (/^https?:\/\//.test(v) || /^\*\./.test(v) || /^[a-z0-9.-]+\.[a-z]{2,}(:\d+)?$/i.test(v)) {
|
|
884
|
+
const origin = /^https?:\/\//.test(v) ? v.replace(/\/+$/, '') : `https://${v}`;
|
|
885
|
+
b.origins.push(origin);
|
|
886
|
+
const wildcard = origin.includes('*');
|
|
887
|
+
b.add(wildcard ? 'differs' : 'mapped', area, `trustedOrigins "${v}"`, wildcard ? 'Wildcard kept. Atlas accepts one wildcard label under a domain you own (https://*.your-domain.com); open or shared-host wildcards are refused.' : 'Allowed origin.', item, 'instance allowed_origins');
|
|
888
|
+
}
|
|
889
|
+
else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(v)) {
|
|
890
|
+
b.doc.redirect_urls.push({ url: v.includes('*') ? v.replace(/\*+$/, '') : v });
|
|
891
|
+
b.add('differs', area, `trustedOrigins "${v}"`, 'An app scheme (native/Expo deep link) becomes a redirect URL. Add native apps under auth_config.nativeApps for passkeys and Sign in with Apple.', item, 'redirect_urls');
|
|
892
|
+
}
|
|
893
|
+
else {
|
|
894
|
+
b.add('manual', area, `trustedOrigins "${v}"`, 'Not an origin Atlas can use; review it.', item);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
else if (item.kind === 'env' || item.kind === 'template') {
|
|
898
|
+
b.origins.push(b.configString(item, 'a trusted origin', 'TRUSTED_ORIGIN', { area, option: 'trustedOrigins' }));
|
|
899
|
+
b.add('mapped', area, 'trustedOrigins', 'Allowed origin from the environment (filled in at apply time).', item, 'instance allowed_origins');
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
b.add('manual', area, 'trustedOrigins', `Cannot read this entry (${describe(item)}).`, item);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
// ───────────────────────────── hooks ─────────────────────────────
|
|
907
|
+
const DB_HOOK_TARGETS = {
|
|
908
|
+
user: {
|
|
909
|
+
'create.before': 'a pre-user-registration Action (can change or refuse the sign-up)',
|
|
910
|
+
'create.after': 'a post-user-registration Action, or a webhook on user.created',
|
|
911
|
+
'update.before': 'validation in your app before calling the update API, or an identity-trait schema',
|
|
912
|
+
'update.after': 'a webhook on user.updated',
|
|
913
|
+
'delete.before': 'your app (Atlas refuses nothing on delete), or a webhook on user.deleted',
|
|
914
|
+
'delete.after': 'a webhook on user.deleted',
|
|
915
|
+
},
|
|
916
|
+
session: {
|
|
917
|
+
'create.before': 'a post-login Action (can deny the sign-in)',
|
|
918
|
+
'create.after': 'a post-login Action, or a webhook on session.created',
|
|
919
|
+
'delete.after': 'a webhook on session.ended / session.revoked',
|
|
920
|
+
},
|
|
921
|
+
account: {
|
|
922
|
+
'create.after': 'a webhook on user.updated (external account linked)',
|
|
923
|
+
},
|
|
924
|
+
verification: {},
|
|
925
|
+
};
|
|
926
|
+
function translateHooks(b, opts) {
|
|
927
|
+
const dbh = (0, upgrade_1.propOf)(opts, 'databaseHooks');
|
|
928
|
+
if (dbh?.kind === 'object') {
|
|
929
|
+
for (const [model, ops] of Object.entries(dbh.props)) {
|
|
930
|
+
if (ops.kind !== 'object')
|
|
931
|
+
continue;
|
|
932
|
+
for (const [op, phases] of Object.entries(ops.props)) {
|
|
933
|
+
if (phases.kind !== 'object')
|
|
934
|
+
continue;
|
|
935
|
+
for (const [phase, fn] of Object.entries(phases.props)) {
|
|
936
|
+
const target = DB_HOOK_TARGETS[model]?.[`${op}.${phase}`] ?? 'a webhook or Action (no direct equivalent: review it)';
|
|
937
|
+
b.add('manual', 'databaseHooks', `databaseHooks.${model}.${op}.${phase}`, `Move this to ${target}.`, fn);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
else if (dbh) {
|
|
943
|
+
b.add('manual', 'databaseHooks', 'databaseHooks', `Cannot read the hooks (${describe(dbh)}); move them to Actions and webhooks.`, dbh);
|
|
944
|
+
}
|
|
945
|
+
const hooks = (0, upgrade_1.propOf)(opts, 'hooks');
|
|
946
|
+
if (hooks) {
|
|
947
|
+
for (const phase of ['before', 'after']) {
|
|
948
|
+
const h = (0, upgrade_1.propOf)(hooks, phase);
|
|
949
|
+
if (!h)
|
|
950
|
+
continue;
|
|
951
|
+
b.add('manual', 'hooks', `hooks.${phase}`, phase === 'before' ? 'Request hooks become Actions: pre-user-registration for sign-up checks, post-login to deny or enrich sign-ins.' : 'Post-request logic becomes a post-login Action or a webhook (user.created, session.created, …).', h);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
function identifyPlugin(v) {
|
|
956
|
+
if (v.kind !== 'call')
|
|
957
|
+
return null;
|
|
958
|
+
const name = v.imported?.name ?? v.callee.split('.').pop() ?? v.callee;
|
|
959
|
+
const module = v.imported?.module ?? '';
|
|
960
|
+
for (const [re, provider] of PAYMENT_PACKAGES) {
|
|
961
|
+
if (re.test(module))
|
|
962
|
+
return { id: `payments:${provider}`, call: v, options: v.args[0] };
|
|
963
|
+
}
|
|
964
|
+
return { id: name, call: v, options: v.args[0] };
|
|
965
|
+
}
|
|
966
|
+
function translatePlugins(b, opts, analysis) {
|
|
967
|
+
const plugins = (0, upgrade_1.propOf)(opts, 'plugins');
|
|
968
|
+
// Methods Better Auth leaves off unless a plugin turns them on.
|
|
969
|
+
b.set('strategies.emailCode', false);
|
|
970
|
+
b.set('strategies.emailLink', false);
|
|
971
|
+
b.set('strategies.passkey', false);
|
|
972
|
+
b.set('strategies.phoneCode', false);
|
|
973
|
+
b.set('strategies.username', false);
|
|
974
|
+
b.set('strategies.anonymous', false);
|
|
975
|
+
b.set('strategies.siwe', false);
|
|
976
|
+
b.set('session.multiSession', false);
|
|
977
|
+
b.set('mfa.factors.passkey', false);
|
|
978
|
+
if (!plugins)
|
|
979
|
+
return;
|
|
980
|
+
if (plugins.kind !== 'array') {
|
|
981
|
+
b.add('manual', 'plugins', 'plugins', `Cannot read the plugin list (${describe(plugins)}).`, plugins);
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
for (const item of plugins.items) {
|
|
985
|
+
const plugin = identifyPlugin(item);
|
|
986
|
+
if (!plugin) {
|
|
987
|
+
b.add('manual', 'plugins', 'plugins[]', `A plugin entry cannot be read (${describe(item)}); review it by hand.`, item);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
b.plugins.push(plugin.id);
|
|
991
|
+
const handler = PLUGIN_HANDLERS[plugin.id];
|
|
992
|
+
if (handler)
|
|
993
|
+
handler(b, plugin, analysis);
|
|
994
|
+
else if (plugin.id.startsWith('payments:'))
|
|
995
|
+
translatePayments(b, plugin);
|
|
996
|
+
else
|
|
997
|
+
b.add('manual', `plugin:${plugin.id}`, `${plugin.id}()`, `No Atlas mapping is known for this plugin${plugin.call.imported ? ` (${plugin.call.imported.module})` : ''}; review what it does.`, plugin.call);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
const PLUGIN_HANDLERS = {
|
|
1001
|
+
twoFactor(b, { options: o, call }) {
|
|
1002
|
+
const area = 'plugin:twoFactor';
|
|
1003
|
+
b.set('mfa.factors.totp', true);
|
|
1004
|
+
b.set('mfaPolicy', 'optional');
|
|
1005
|
+
b.add('mapped', area, 'twoFactor()', 'Authenticator-app (TOTP) second factor, optional per user. The exporter decrypts two-factor secrets in your app and seals them to the instance, so enrolled users keep their authenticator; with a plain JSON import (atlas import) they enrol again.', call, 'auth_config.mfa.factors.totp');
|
|
1006
|
+
const totp = (0, upgrade_1.propOf)(o, 'totpOptions');
|
|
1007
|
+
const digits = num((0, upgrade_1.propOf)(totp, 'digits'));
|
|
1008
|
+
if (digits !== undefined) {
|
|
1009
|
+
const ok = digits === 6 || digits === 8;
|
|
1010
|
+
b.set('mfa.totp.digits', ok ? digits : 6);
|
|
1011
|
+
b.add(ok ? 'mapped' : 'differs', area, 'twoFactor.totpOptions.digits', ok ? `${digits} digits.` : `Atlas supports 6 or 8 digits; set to 6 (Better Auth: ${digits}).`, (0, upgrade_1.propOf)(totp, 'digits'), 'auth_config.mfa.totp.digits');
|
|
1012
|
+
}
|
|
1013
|
+
const period = num((0, upgrade_1.propOf)(totp, 'period'));
|
|
1014
|
+
if (period !== undefined) {
|
|
1015
|
+
const ok = period === 30 || period === 60;
|
|
1016
|
+
b.set('mfa.totp.period', ok ? period : 30);
|
|
1017
|
+
b.add(ok ? 'mapped' : 'differs', area, 'twoFactor.totpOptions.period', ok ? `${period}s.` : `Atlas supports 30 or 60 seconds; set to 30 (Better Auth: ${period}).`, (0, upgrade_1.propOf)(totp, 'period'), 'auth_config.mfa.totp.period');
|
|
1018
|
+
}
|
|
1019
|
+
const otp = (0, upgrade_1.propOf)(o, 'otpOptions');
|
|
1020
|
+
if ((0, upgrade_1.propOf)(otp, 'sendOTP')) {
|
|
1021
|
+
b.set('mfa.factors.emailCode', true);
|
|
1022
|
+
b.add('differs', area, 'twoFactor.otpOptions.sendOTP', 'One-time codes as a second factor: Atlas sends them by email through your email provider. If you sent them by SMS, turn on mfa.factors.sms (and configure your SMS provider) instead.', (0, upgrade_1.propOf)(otp, 'sendOTP'), 'auth_config.mfa.factors.emailCode');
|
|
1023
|
+
}
|
|
1024
|
+
const backup = (0, upgrade_1.propOf)(o, 'backupCodeOptions');
|
|
1025
|
+
const amount = num((0, upgrade_1.propOf)(backup, 'amount'));
|
|
1026
|
+
if (amount !== undefined) {
|
|
1027
|
+
b.set('mfa.backupCodes.count', clamp(amount, 1, 50));
|
|
1028
|
+
b.add('mapped', area, 'twoFactor.backupCodeOptions.amount', `${amount} backup codes.`, (0, upgrade_1.propOf)(backup, 'amount'), 'auth_config.mfa.backupCodes.count');
|
|
1029
|
+
}
|
|
1030
|
+
if ((0, upgrade_1.propOf)(backup, 'length') || (0, upgrade_1.propOf)(backup, 'customBackupCodesGenerate'))
|
|
1031
|
+
b.add('differs', area, 'twoFactor.backupCodeOptions.length', 'Atlas generates backup codes in its own format.', (0, upgrade_1.propOf)(backup, 'length') ?? (0, upgrade_1.propOf)(backup, 'customBackupCodesGenerate'));
|
|
1032
|
+
const trust = (0, upgrade_1.propOf)(o, 'trustDeviceMaxAge');
|
|
1033
|
+
const trustS = num(trust) ?? 30 * DAY;
|
|
1034
|
+
b.set('mfa.rememberDevice.enabled', true);
|
|
1035
|
+
b.set('mfa.rememberDevice.days', clamp(Math.round(trustS / DAY) || 1, 1, 365));
|
|
1036
|
+
b.add('mapped', area, 'twoFactor.trustDeviceMaxAge', `A trusted device skips the second factor for ${Math.round(trustS / DAY) || 1} day(s).`, trust ?? call, 'auth_config.mfa.rememberDevice');
|
|
1037
|
+
if ((0, upgrade_1.propOf)(o, 'issuer'))
|
|
1038
|
+
b.add('differs', area, 'twoFactor.issuer', 'The authenticator-app label is your Atlas application name.', (0, upgrade_1.propOf)(o, 'issuer'));
|
|
1039
|
+
if ((0, upgrade_1.propOf)(o, 'skipVerificationOnEnable'))
|
|
1040
|
+
b.add('differs', area, 'twoFactor.skipVerificationOnEnable', 'Atlas always confirms a first code before turning two-factor on.', (0, upgrade_1.propOf)(o, 'skipVerificationOnEnable'));
|
|
1041
|
+
if ((0, upgrade_1.propOf)(o, 'allowPasswordless'))
|
|
1042
|
+
b.add('mapped', area, 'twoFactor.allowPasswordless', 'Passkeys and other passwordless methods can stand in where configured.', (0, upgrade_1.propOf)(o, 'allowPasswordless'));
|
|
1043
|
+
},
|
|
1044
|
+
passkey(b, { options: o, call }) {
|
|
1045
|
+
const area = 'plugin:passkey';
|
|
1046
|
+
b.set('strategies.passkey', true);
|
|
1047
|
+
b.set('mfa.factors.passkey', true);
|
|
1048
|
+
b.add('mapped', area, 'passkey()', 'Passkey sign-in on.', call, 'auth_config.strategies.passkey');
|
|
1049
|
+
const rpID = (0, upgrade_1.propOf)(o, 'rpID');
|
|
1050
|
+
b.add('manual', area, 'passkey.rpID', `A passkey works only on the relying-party id it was registered for${str(rpID) ? ` ("${str(rpID)}")` : ' (your app\'s domain)'}. Serve Atlas from a custom domain under that domain (e.g. auth.${str(rpID) ?? 'your-domain.com'}) so existing passkeys keep working; the import report lists the passkeys carried over.`, rpID ?? call);
|
|
1051
|
+
if ((0, upgrade_1.propOf)(o, 'origin'))
|
|
1052
|
+
b.add('mapped', area, 'passkey.origin', 'The allowed origins carry the WebAuthn origin check.', (0, upgrade_1.propOf)(o, 'origin'), 'instance allowed_origins');
|
|
1053
|
+
if ((0, upgrade_1.propOf)(o, 'rpName'))
|
|
1054
|
+
b.add('differs', area, 'passkey.rpName', 'The name shown in the passkey prompt is your Atlas application name.', (0, upgrade_1.propOf)(o, 'rpName'));
|
|
1055
|
+
const sel = (0, upgrade_1.propOf)(o, 'authenticatorSelection');
|
|
1056
|
+
const attach = str((0, upgrade_1.propOf)(sel, 'authenticatorAttachment'));
|
|
1057
|
+
if (attach === 'platform' || attach === 'cross-platform') {
|
|
1058
|
+
b.set('passkeyPolicy.attachment', attach);
|
|
1059
|
+
b.add('mapped', area, 'passkey.authenticatorSelection.authenticatorAttachment', `${attach} authenticators.`, (0, upgrade_1.propOf)(sel, 'authenticatorAttachment'), 'auth_config.passkeyPolicy.attachment');
|
|
1060
|
+
}
|
|
1061
|
+
const uv = str((0, upgrade_1.propOf)(sel, 'userVerification'));
|
|
1062
|
+
if (uv) {
|
|
1063
|
+
b.set('passkeyPolicy.requireUserVerification', uv === 'required');
|
|
1064
|
+
b.add(uv === 'discouraged' ? 'differs' : 'mapped', area, 'passkey.authenticatorSelection.userVerification', uv === 'required' ? 'User verification required.' : 'User verification preferred, not required.', (0, upgrade_1.propOf)(sel, 'userVerification'), 'auth_config.passkeyPolicy.requireUserVerification');
|
|
1065
|
+
}
|
|
1066
|
+
if ((0, upgrade_1.propOf)(sel, 'residentKey'))
|
|
1067
|
+
b.add('mapped', area, 'passkey.authenticatorSelection.residentKey', 'Atlas registers discoverable passkeys.', (0, upgrade_1.propOf)(sel, 'residentKey'));
|
|
1068
|
+
},
|
|
1069
|
+
magicLink(b, { options: o, call }) {
|
|
1070
|
+
const area = 'plugin:magicLink';
|
|
1071
|
+
b.set('strategies.emailLink', true);
|
|
1072
|
+
b.add('mapped', area, 'magicLink()', 'Email sign-in links on. Atlas sends them through your email provider; customise the "magic link" email template instead of sendMagicLink.', call, 'auth_config.strategies.emailLink');
|
|
1073
|
+
const ttl = num((0, upgrade_1.propOf)(o, 'expiresIn')) ?? 300;
|
|
1074
|
+
b.set('verification.magicLink.ttlMinutes', clamp(Math.ceil(ttl / 60), 1, 1440));
|
|
1075
|
+
b.add('mapped', area, 'magicLink.expiresIn', `${Math.ceil(ttl / 60)} min link lifetime (shared with verification and reset links).`, (0, upgrade_1.propOf)(o, 'expiresIn') ?? call, 'auth_config.verification.magicLink.ttlMinutes');
|
|
1076
|
+
if (bool((0, upgrade_1.propOf)(o, 'disableSignUp')))
|
|
1077
|
+
b.add('differs', area, 'magicLink.disableSignUp', 'Atlas cannot close sign-up for one method only; use signUpMode "restricted" to close it for every method.', (0, upgrade_1.propOf)(o, 'disableSignUp'));
|
|
1078
|
+
if ((0, upgrade_1.propOf)(o, 'generateToken') || (0, upgrade_1.propOf)(o, 'storeToken'))
|
|
1079
|
+
b.add('differs', area, 'magicLink.generateToken', 'Atlas generates and stores link tokens itself (hashed).', (0, upgrade_1.propOf)(o, 'generateToken') ?? (0, upgrade_1.propOf)(o, 'storeToken'));
|
|
1080
|
+
},
|
|
1081
|
+
emailOTP(b, { options: o, call }) {
|
|
1082
|
+
const area = 'plugin:emailOTP';
|
|
1083
|
+
b.set('strategies.emailCode', true);
|
|
1084
|
+
b.add('mapped', area, 'emailOTP()', 'Email one-time-code sign-in on. Atlas sends the codes through your email provider; customise the email templates instead of sendVerificationOTP.', call, 'auth_config.strategies.emailCode');
|
|
1085
|
+
const len = num((0, upgrade_1.propOf)(o, 'otpLength'));
|
|
1086
|
+
if (len !== undefined) {
|
|
1087
|
+
b.set('verification.emailCode.length', clamp(len, 4, 10));
|
|
1088
|
+
b.add(len >= 4 && len <= 10 ? 'mapped' : 'differs', area, 'emailOTP.otpLength', `${clamp(len, 4, 10)} digits.`, (0, upgrade_1.propOf)(o, 'otpLength'), 'auth_config.verification.emailCode.length');
|
|
1089
|
+
}
|
|
1090
|
+
const ttl = num((0, upgrade_1.propOf)(o, 'expiresIn'));
|
|
1091
|
+
if (ttl !== undefined) {
|
|
1092
|
+
const min = clamp(Math.ceil(ttl / 60), 1, 60);
|
|
1093
|
+
b.set('verification.emailCode.ttlMinutes', min);
|
|
1094
|
+
b.add(ttl / 60 <= 60 ? 'mapped' : 'differs', area, 'emailOTP.expiresIn', `${min} min code lifetime${ttl / 60 > 60 ? ' (Atlas maximum)' : ''}.`, (0, upgrade_1.propOf)(o, 'expiresIn'), 'auth_config.verification.emailCode.ttlMinutes');
|
|
1095
|
+
}
|
|
1096
|
+
const attempts = num((0, upgrade_1.propOf)(o, 'allowedAttempts'));
|
|
1097
|
+
if (attempts !== undefined) {
|
|
1098
|
+
b.set('verification.emailCode.maxAttempts', clamp(attempts, 1, 10));
|
|
1099
|
+
b.add('mapped', area, 'emailOTP.allowedAttempts', `${clamp(attempts, 1, 10)} attempts.`, (0, upgrade_1.propOf)(o, 'allowedAttempts'), 'auth_config.verification.emailCode.maxAttempts');
|
|
1100
|
+
}
|
|
1101
|
+
if (bool((0, upgrade_1.propOf)(o, 'overrideDefaultEmailVerification')) || bool((0, upgrade_1.propOf)(o, 'sendVerificationOnSignUp'))) {
|
|
1102
|
+
b.set('accountUx.emailVerification', 'code');
|
|
1103
|
+
b.add('mapped', area, 'emailOTP.overrideDefaultEmailVerification', 'Email verification uses a code instead of a link.', (0, upgrade_1.propOf)(o, 'overrideDefaultEmailVerification') ?? (0, upgrade_1.propOf)(o, 'sendVerificationOnSignUp'), 'auth_config.accountUx.emailVerification');
|
|
1104
|
+
}
|
|
1105
|
+
const resend = str((0, upgrade_1.propOf)(o, 'resendStrategy'));
|
|
1106
|
+
if (resend) {
|
|
1107
|
+
b.set('verification.emailCode.resendSameCode', resend === 'reuse');
|
|
1108
|
+
b.add('mapped', area, 'emailOTP.resendStrategy', resend === 'reuse' ? 'A resend repeats the same code.' : 'A resend issues a new code.', (0, upgrade_1.propOf)(o, 'resendStrategy'), 'auth_config.verification.emailCode.resendSameCode');
|
|
1109
|
+
}
|
|
1110
|
+
if (bool((0, upgrade_1.propOf)(o, 'disableSignUp')))
|
|
1111
|
+
b.add('differs', area, 'emailOTP.disableSignUp', 'Atlas cannot close sign-up for one method only; use signUpMode "restricted".', (0, upgrade_1.propOf)(o, 'disableSignUp'));
|
|
1112
|
+
if ((0, upgrade_1.propOf)(o, 'generateOTP') || (0, upgrade_1.propOf)(o, 'storeOTP'))
|
|
1113
|
+
b.add('differs', area, 'emailOTP.generateOTP', 'Atlas generates and stores codes itself (hashed).', (0, upgrade_1.propOf)(o, 'generateOTP') ?? (0, upgrade_1.propOf)(o, 'storeOTP'));
|
|
1114
|
+
if ((0, upgrade_1.propOf)(o, 'changeEmail'))
|
|
1115
|
+
b.add('mapped', area, 'emailOTP.changeEmail', 'An email change is proven with a code to the new address.', (0, upgrade_1.propOf)(o, 'changeEmail'));
|
|
1116
|
+
},
|
|
1117
|
+
phoneNumber(b, { options: o, call }) {
|
|
1118
|
+
const area = 'plugin:phoneNumber';
|
|
1119
|
+
b.set('strategies.phoneCode', true);
|
|
1120
|
+
b.add('mapped', area, 'phoneNumber()', 'SMS code sign-in on. Configure your SMS provider (your own account) in Atlas; sendOTP is no longer used. Phone numbers and their verified state are imported.', call, 'auth_config.strategies.phoneCode');
|
|
1121
|
+
const len = num((0, upgrade_1.propOf)(o, 'otpLength'));
|
|
1122
|
+
if (len !== undefined) {
|
|
1123
|
+
b.set('verification.smsCode.length', clamp(len, 4, 10));
|
|
1124
|
+
b.add('mapped', area, 'phoneNumber.otpLength', `${clamp(len, 4, 10)} digits.`, (0, upgrade_1.propOf)(o, 'otpLength'), 'auth_config.verification.smsCode.length');
|
|
1125
|
+
}
|
|
1126
|
+
const ttl = num((0, upgrade_1.propOf)(o, 'expiresIn'));
|
|
1127
|
+
if (ttl !== undefined) {
|
|
1128
|
+
b.set('verification.smsCode.ttlMinutes', clamp(Math.ceil(ttl / 60), 1, 60));
|
|
1129
|
+
b.add('mapped', area, 'phoneNumber.expiresIn', `${clamp(Math.ceil(ttl / 60), 1, 60)} min code lifetime.`, (0, upgrade_1.propOf)(o, 'expiresIn'), 'auth_config.verification.smsCode.ttlMinutes');
|
|
1130
|
+
}
|
|
1131
|
+
const attempts = num((0, upgrade_1.propOf)(o, 'allowedAttempts'));
|
|
1132
|
+
if (attempts !== undefined) {
|
|
1133
|
+
b.set('verification.smsCode.maxAttempts', clamp(attempts, 1, 10));
|
|
1134
|
+
b.add('mapped', area, 'phoneNumber.allowedAttempts', `${clamp(attempts, 1, 10)} attempts.`, (0, upgrade_1.propOf)(o, 'allowedAttempts'), 'auth_config.verification.smsCode.maxAttempts');
|
|
1135
|
+
}
|
|
1136
|
+
const signUp = (0, upgrade_1.propOf)(o, 'signUpOnVerification');
|
|
1137
|
+
if (signUp)
|
|
1138
|
+
b.add('differs', area, 'phoneNumber.signUpOnVerification', 'A verified phone number signs up a new user with no email; Atlas does not need a temporary email (getTempEmail).', signUp);
|
|
1139
|
+
if ((0, upgrade_1.propOf)(o, 'phoneNumberValidator'))
|
|
1140
|
+
b.add('differs', area, 'phoneNumber.phoneNumberValidator', 'Atlas validates numbers as E.164; restrict countries with attackProtection.countries or smsPumping prefixes.', (0, upgrade_1.propOf)(o, 'phoneNumberValidator'));
|
|
1141
|
+
},
|
|
1142
|
+
username(b, { options: o, call }) {
|
|
1143
|
+
const area = 'plugin:username';
|
|
1144
|
+
b.set('strategies.username', true);
|
|
1145
|
+
b.add('mapped', area, 'username()', 'Username sign-in on; usernames are imported.', call, 'auth_config.strategies.username');
|
|
1146
|
+
const min = num((0, upgrade_1.propOf)(o, 'minUsernameLength'));
|
|
1147
|
+
const max = num((0, upgrade_1.propOf)(o, 'maxUsernameLength'));
|
|
1148
|
+
b.set('accountUx.username.minLength', clamp(min ?? 3, 1, 64));
|
|
1149
|
+
b.set('accountUx.username.maxLength', clamp(max ?? 30, clamp(min ?? 3, 1, 64), 64));
|
|
1150
|
+
b.add(max !== undefined && max > 64 ? 'differs' : 'mapped', area, 'username.minUsernameLength/maxUsernameLength', `${clamp(min ?? 3, 1, 64)}-${clamp(max ?? 30, 1, 64)} characters${max !== undefined && max > 64 ? ' (Atlas maximum 64)' : ''}.`, (0, upgrade_1.propOf)(o, 'minUsernameLength') ?? (0, upgrade_1.propOf)(o, 'maxUsernameLength') ?? call, 'auth_config.accountUx.username');
|
|
1151
|
+
if ((0, upgrade_1.propOf)(o, 'usernameValidator'))
|
|
1152
|
+
b.add('differs', area, 'username.usernameValidator', 'Atlas validates with a character set: pick "default" (letters, digits, . _ -), "alphanumeric" or "alphanumeric_underscore" (accountUx.username.charset).', (0, upgrade_1.propOf)(o, 'usernameValidator'), 'auth_config.accountUx.username.charset');
|
|
1153
|
+
if ((0, upgrade_1.propOf)(o, 'usernameNormalization'))
|
|
1154
|
+
b.add('differs', area, 'username.usernameNormalization', 'Atlas compares usernames case-insensitively.', (0, upgrade_1.propOf)(o, 'usernameNormalization'));
|
|
1155
|
+
if ((0, upgrade_1.propOf)(o, 'displayUsernameValidator') || (0, upgrade_1.propOf)(o, 'displayUsernameNormalization'))
|
|
1156
|
+
b.add('manual', area, 'username.displayUsername', 'displayUsername is imported into public metadata (displayUsername).', (0, upgrade_1.propOf)(o, 'displayUsernameValidator') ?? (0, upgrade_1.propOf)(o, 'displayUsernameNormalization'));
|
|
1157
|
+
},
|
|
1158
|
+
anonymous(b, { options: o, call }) {
|
|
1159
|
+
const area = 'plugin:anonymous';
|
|
1160
|
+
b.set('strategies.anonymous', true);
|
|
1161
|
+
b.add('mapped', area, 'anonymous()', 'Guest (anonymous) sign-in on; a guest who signs up keeps their user.', call, 'auth_config.strategies.anonymous');
|
|
1162
|
+
if ((0, upgrade_1.propOf)(o, 'onLinkAccount'))
|
|
1163
|
+
b.add('manual', area, 'anonymous.onLinkAccount', 'Move this to a webhook on user.updated (a guest upgraded to a full account) or a post-login Action.', (0, upgrade_1.propOf)(o, 'onLinkAccount'));
|
|
1164
|
+
if ((0, upgrade_1.propOf)(o, 'emailDomainName') || (0, upgrade_1.propOf)(o, 'generateRandomEmail'))
|
|
1165
|
+
b.add('mapped', area, 'anonymous.emailDomainName', 'Atlas guests have no placeholder email.', (0, upgrade_1.propOf)(o, 'emailDomainName') ?? (0, upgrade_1.propOf)(o, 'generateRandomEmail'));
|
|
1166
|
+
if ((0, upgrade_1.propOf)(o, 'disableDeleteAnonymousUser'))
|
|
1167
|
+
b.add('differs', area, 'anonymous.disableDeleteAnonymousUser', 'Atlas keeps the guest user and upgrades it in place when it signs up.', (0, upgrade_1.propOf)(o, 'disableDeleteAnonymousUser'));
|
|
1168
|
+
},
|
|
1169
|
+
organization(b, { options: o, call }) {
|
|
1170
|
+
translateOrganization(b, o, call);
|
|
1171
|
+
},
|
|
1172
|
+
admin(b, { options: o, call }) {
|
|
1173
|
+
const area = 'plugin:admin';
|
|
1174
|
+
b.add('mapped', area, 'admin()', 'User management, bans and sessions are in the Atlas dashboard and Backend API (users list/ban/unban/delete, sessions revoke). Bans are imported.', call);
|
|
1175
|
+
const defaultRole = str((0, upgrade_1.propOf)(o, 'defaultRole')) ?? 'user';
|
|
1176
|
+
const adminRoles = (0, upgrade_1.propOf)(o, 'adminRoles');
|
|
1177
|
+
const roles = new Set([defaultRole, 'admin']);
|
|
1178
|
+
if (adminRoles?.kind === 'array') {
|
|
1179
|
+
for (const r of adminRoles.items) {
|
|
1180
|
+
const name = str(r);
|
|
1181
|
+
if (name)
|
|
1182
|
+
roles.add(name);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
else if (str(adminRoles)) {
|
|
1186
|
+
roles.add(str(adminRoles));
|
|
1187
|
+
}
|
|
1188
|
+
const custom = (0, upgrade_1.propOf)(o, 'roles');
|
|
1189
|
+
if (custom?.kind === 'object') {
|
|
1190
|
+
for (const r of Object.keys(custom.props))
|
|
1191
|
+
roles.add(r);
|
|
1192
|
+
}
|
|
1193
|
+
const existing = b.get('identityTraits.traits') ?? [];
|
|
1194
|
+
if (!existing.some((t) => t.key === 'role')) {
|
|
1195
|
+
b.set('identityTraits.traits', [...existing, { key: 'role', type: 'enum', enum: [...roles], userEditable: false }]);
|
|
1196
|
+
}
|
|
1197
|
+
b.add('differs', area, 'admin.roles', `Application roles (${[...roles].join(', ')}) are kept on each user as public_metadata.role (a typed, read-only trait), so your checks read it from the session token's metadata. Atlas roles proper are organization roles.`, custom ?? adminRoles ?? call, 'auth_config.identityTraits (role)');
|
|
1198
|
+
if ((0, upgrade_1.propOf)(o, 'adminUserIds'))
|
|
1199
|
+
b.add('manual', area, 'admin.adminUserIds', 'Give these people dashboard access (team members) or set their role trait to an admin role after the import.', (0, upgrade_1.propOf)(o, 'adminUserIds'));
|
|
1200
|
+
if ((0, upgrade_1.propOf)(o, 'ac'))
|
|
1201
|
+
b.add('manual', area, 'admin.ac', 'Access-control statements for app roles have no instance-level equivalent; enforce them in your app from the role trait.', (0, upgrade_1.propOf)(o, 'ac'));
|
|
1202
|
+
const imp = (0, upgrade_1.propOf)(o, 'impersonationSessionDuration');
|
|
1203
|
+
b.add('differs', area, 'admin.impersonation', `Impersonation uses actor tokens (POST /v1/actor_tokens, or "Impersonate" in the dashboard); the session carries an act claim${num(imp) ? `. Set the actor token lifetime per request (Better Auth: ${humanDuration(num(imp))})` : ''}.`, imp ?? call);
|
|
1204
|
+
if ((0, upgrade_1.propOf)(o, 'allowImpersonatingAdmins'))
|
|
1205
|
+
b.add('differs', area, 'admin.allowImpersonatingAdmins', 'Who may impersonate is decided by who can create actor tokens (your secret key holders and dashboard admins).', (0, upgrade_1.propOf)(o, 'allowImpersonatingAdmins'));
|
|
1206
|
+
for (const k of ['defaultBanReason', 'defaultBanExpiresIn', 'bannedUserMessage']) {
|
|
1207
|
+
if ((0, upgrade_1.propOf)(o, k))
|
|
1208
|
+
b.add('differs', area, `admin.${k}`, k === 'bannedUserMessage' ? 'The banned message is localised text on the hosted pages.' : 'Pass the reason / expiry on each ban call.', (0, upgrade_1.propOf)(o, k));
|
|
1209
|
+
}
|
|
1210
|
+
},
|
|
1211
|
+
apiKey(b, { options: o, call }) {
|
|
1212
|
+
const area = 'plugin:apiKey';
|
|
1213
|
+
b.set('endUserApiKeys.selfServiceEnabled', true);
|
|
1214
|
+
b.add('mapped', area, 'apiKey()', 'End-user API keys (users manage their own keys; servers verify them with the Backend API). Existing keys are stored hashed and cannot be carried over: users create new ones.', call, 'auth_config.endUserApiKeys.selfServiceEnabled');
|
|
1215
|
+
const prefix = str((0, upgrade_1.propOf)(o, 'defaultPrefix'));
|
|
1216
|
+
if (prefix) {
|
|
1217
|
+
const cleaned = prefix.replace(/[^A-Za-z0-9_]/g, '').replace(/_+$/, '').slice(0, 32);
|
|
1218
|
+
if (cleaned && /^[A-Za-z]/.test(cleaned)) {
|
|
1219
|
+
b.set('endUserApiKeys.keyPrefix', cleaned);
|
|
1220
|
+
b.add(cleaned === prefix ? 'mapped' : 'differs', area, 'apiKey.defaultPrefix', cleaned === prefix ? `Prefix "${prefix}".` : `Prefix "${cleaned}" (Atlas adds its own separator; Better Auth: "${prefix}").`, (0, upgrade_1.propOf)(o, 'defaultPrefix'), 'auth_config.endUserApiKeys.keyPrefix');
|
|
1221
|
+
}
|
|
1222
|
+
else {
|
|
1223
|
+
b.add('manual', area, 'apiKey.defaultPrefix', `"${prefix}" is not a valid prefix (a letter, then letters, digits or _).`, (0, upgrade_1.propOf)(o, 'defaultPrefix'));
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
const exp = (0, upgrade_1.propOf)(o, 'keyExpiration');
|
|
1227
|
+
const def = (0, upgrade_1.propOf)(exp, 'defaultExpiresIn');
|
|
1228
|
+
if (def) {
|
|
1229
|
+
const ms = num(def);
|
|
1230
|
+
if (ms !== undefined) {
|
|
1231
|
+
b.set('endUserApiKeys.defaultExpiresInSeconds', clamp(Math.round(ms / 1000), 60, 10 * 365 * DAY));
|
|
1232
|
+
b.add('mapped', area, 'apiKey.keyExpiration.defaultExpiresIn', `Keys expire after ${humanDuration(Math.round(ms / 1000))} by default.`, def, 'auth_config.endUserApiKeys.defaultExpiresInSeconds');
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
const maxDays = num((0, upgrade_1.propOf)(exp, 'maxExpiresIn'));
|
|
1236
|
+
if (maxDays !== undefined || bool((0, upgrade_1.propOf)(exp, 'disableCustomExpiresTime'))) {
|
|
1237
|
+
const maxS = maxDays !== undefined ? maxDays * DAY : (num(def) ?? 365 * DAY * 1000) / 1000;
|
|
1238
|
+
b.set('endUserApiKeys.maxExpiresInSeconds', clamp(Math.round(maxS), 60, 10 * 365 * DAY));
|
|
1239
|
+
b.add('mapped', area, 'apiKey.keyExpiration.maxExpiresIn', `Longest key lifetime ${humanDuration(Math.round(maxS))}.`, (0, upgrade_1.propOf)(exp, 'maxExpiresIn') ?? (0, upgrade_1.propOf)(exp, 'disableCustomExpiresTime'), 'auth_config.endUserApiKeys.maxExpiresInSeconds');
|
|
1240
|
+
}
|
|
1241
|
+
if ((0, upgrade_1.propOf)(exp, 'minExpiresIn'))
|
|
1242
|
+
b.add('differs', area, 'apiKey.keyExpiration.minExpiresIn', 'Atlas accepts any lifetime from 60 seconds up.', (0, upgrade_1.propOf)(exp, 'minExpiresIn'));
|
|
1243
|
+
const rl = (0, upgrade_1.propOf)(o, 'rateLimit');
|
|
1244
|
+
if (rl)
|
|
1245
|
+
b.add('differs', area, 'apiKey.rateLimit', 'Per-key request quotas (remaining / refill) are not enforced by Atlas; count requests in your API or with the entitlements usage API.', rl);
|
|
1246
|
+
const refs = str((0, upgrade_1.propOf)(o, 'references'));
|
|
1247
|
+
if (refs === 'organization') {
|
|
1248
|
+
b.set('endUserApiKeys.allowOrganizationKeys', true);
|
|
1249
|
+
b.add('mapped', area, 'apiKey.references', 'Organization-owned keys on.', (0, upgrade_1.propOf)(o, 'references'), 'auth_config.endUserApiKeys.allowOrganizationKeys');
|
|
1250
|
+
}
|
|
1251
|
+
if ((0, upgrade_1.propOf)(o, 'enableSessionForAPIKeys'))
|
|
1252
|
+
b.add('differs', area, 'apiKey.enableSessionForAPIKeys', 'An API key is verified with the Backend API (api_keys verify), not turned into a session.', (0, upgrade_1.propOf)(o, 'enableSessionForAPIKeys'));
|
|
1253
|
+
if ((0, upgrade_1.propOf)(o, 'permissions'))
|
|
1254
|
+
b.add('manual', area, 'apiKey.permissions', 'Key permissions become scopes: list them in endUserApiKeys.allowedScopes.', (0, upgrade_1.propOf)(o, 'permissions'), 'auth_config.endUserApiKeys.allowedScopes');
|
|
1255
|
+
if ((0, upgrade_1.propOf)(o, 'enableMetadata'))
|
|
1256
|
+
b.add('mapped', area, 'apiKey.enableMetadata', 'Keys carry claims (endUserApiKeys.allowedClaimNames).', (0, upgrade_1.propOf)(o, 'enableMetadata'));
|
|
1257
|
+
if ((0, upgrade_1.propOf)(o, 'apiKeyHeaders'))
|
|
1258
|
+
b.add('manual', area, 'apiKey.apiKeyHeaders', 'Your server reads the key from whichever header it likes and verifies it with the Backend API.', (0, upgrade_1.propOf)(o, 'apiKeyHeaders'));
|
|
1259
|
+
},
|
|
1260
|
+
jwt(b, { options: o, call }) {
|
|
1261
|
+
const area = 'plugin:jwt';
|
|
1262
|
+
b.add('mapped', area, 'jwt()', 'Every Atlas session is a signed JWT with a public JWKS. Point services that verified Better Auth JWTs at the Atlas JWKS (/.well-known/jwks.json on your Frontend API host).', call);
|
|
1263
|
+
const jwt = (0, upgrade_1.propOf)(o, 'jwt');
|
|
1264
|
+
const aud = (0, upgrade_1.propOf)(jwt, 'audience');
|
|
1265
|
+
if (aud) {
|
|
1266
|
+
const v = aud.kind === 'array' ? aud.items.map(str).filter((s) => Boolean(s)) : str(aud);
|
|
1267
|
+
if (v) {
|
|
1268
|
+
b.set('session.audience', v);
|
|
1269
|
+
b.add('mapped', area, 'jwt.jwt.audience', 'Session tokens carry this audience.', aud, 'auth_config.session.audience');
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
const exp = durationSeconds((0, upgrade_1.propOf)(jwt, 'expirationTime'));
|
|
1273
|
+
if (exp !== undefined) {
|
|
1274
|
+
const ttl = clamp(exp, 30, 900);
|
|
1275
|
+
b.set('session.jwtTtlSeconds', ttl);
|
|
1276
|
+
b.add(ttl === exp ? 'mapped' : 'differs', area, 'jwt.jwt.expirationTime', `Session tokens live ${ttl}s and are refreshed automatically by the SDKs${ttl !== exp ? ` (Atlas allows 30-900 s; Better Auth: ${exp}s). For a longer-lived token for another service, use a JWT template with its own lifetime` : ''}.`, (0, upgrade_1.propOf)(jwt, 'expirationTime'), 'auth_config.session.jwtTtlSeconds');
|
|
1277
|
+
}
|
|
1278
|
+
if ((0, upgrade_1.propOf)(jwt, 'issuer'))
|
|
1279
|
+
b.add('differs', area, 'jwt.jwt.issuer', 'The issuer is your Atlas Frontend API URL; update verifiers to accept it.', (0, upgrade_1.propOf)(jwt, 'issuer'));
|
|
1280
|
+
const definePayload = (0, upgrade_1.propOf)(jwt, 'definePayload');
|
|
1281
|
+
if (definePayload) {
|
|
1282
|
+
b.add('manual', area, 'jwt.jwt.definePayload', 'Custom claims become the "session" JWT template (claims built from user fields and metadata); add them under jwt_templates.', definePayload, 'jwt_templates session');
|
|
1283
|
+
}
|
|
1284
|
+
if ((0, upgrade_1.propOf)(jwt, 'getSubject'))
|
|
1285
|
+
b.add('differs', area, 'jwt.jwt.getSubject', 'sub is the Atlas user id; the Better Auth id is in the external_id claim.', (0, upgrade_1.propOf)(jwt, 'getSubject'));
|
|
1286
|
+
const jwks = (0, upgrade_1.propOf)(o, 'jwks');
|
|
1287
|
+
const alg = str((0, upgrade_1.propOf)((0, upgrade_1.propOf)(jwks, 'keyPairConfig'), 'alg'));
|
|
1288
|
+
if (alg)
|
|
1289
|
+
b.add(alg === 'RS256' ? 'mapped' : 'differs', area, 'jwt.jwks.keyPairConfig.alg', alg === 'RS256' ? 'RS256.' : `Atlas signs sessions with RS256 (Better Auth: ${alg}); JWT templates can use HS256.`, (0, upgrade_1.propOf)((0, upgrade_1.propOf)(jwks, 'keyPairConfig'), 'alg'));
|
|
1290
|
+
if ((0, upgrade_1.propOf)(jwks, 'rotationInterval'))
|
|
1291
|
+
b.add('mapped', area, 'jwt.jwks.rotationInterval', 'Rotate signing keys from the dashboard or Backend API (old keys stay published during the overlap).', (0, upgrade_1.propOf)(jwks, 'rotationInterval'));
|
|
1292
|
+
},
|
|
1293
|
+
bearer(b, { call }) {
|
|
1294
|
+
b.add('mapped', 'plugin:bearer', 'bearer()', 'Atlas SDKs send the session token as an Authorization: Bearer header for API calls; servers verify it with authenticateRequest().', call);
|
|
1295
|
+
},
|
|
1296
|
+
multiSession(b, { options: o, call }) {
|
|
1297
|
+
b.set('session.multiSession', true);
|
|
1298
|
+
b.add('mapped', 'plugin:multiSession', 'multiSession()', 'Several accounts signed in on one browser, with an account switcher.', call, 'auth_config.session.multiSession');
|
|
1299
|
+
if ((0, upgrade_1.propOf)(o, 'maximumSessions'))
|
|
1300
|
+
b.add('differs', 'plugin:multiSession', 'multiSession.maximumSessions', 'Atlas does not cap the accounts per browser. session.maxConcurrentSessions caps sessions per user instead.', (0, upgrade_1.propOf)(o, 'maximumSessions'));
|
|
1301
|
+
},
|
|
1302
|
+
oneTap(b, { options: o, call }) {
|
|
1303
|
+
const area = 'plugin:oneTap';
|
|
1304
|
+
b.add('mapped', area, 'oneTap()', 'Google One Tap works with the Google provider (the sign-in component shows it when Google is on).', call, 'oauth_providers google');
|
|
1305
|
+
if (!b.doc.oauth_providers.some((p) => p.provider === 'google')) {
|
|
1306
|
+
b.add('manual', area, 'oneTap.clientId', 'Configure the Google provider (same OAuth client as One Tap) so One Tap has a client id.', (0, upgrade_1.propOf)(o, 'clientId') ?? call, 'oauth_providers google');
|
|
1307
|
+
}
|
|
1308
|
+
if (bool((0, upgrade_1.propOf)(o, 'disableSignup')))
|
|
1309
|
+
b.add('differs', area, 'oneTap.disableSignup', 'Set allow_sign_up false on the Google provider (applies to every Google sign-in).', (0, upgrade_1.propOf)(o, 'disableSignup'));
|
|
1310
|
+
},
|
|
1311
|
+
siwe(b, { options: o, call }) {
|
|
1312
|
+
const area = 'plugin:siwe';
|
|
1313
|
+
b.set('strategies.siwe', true);
|
|
1314
|
+
b.add('mapped', area, 'siwe()', 'Sign-In with Ethereum on; wallet addresses are imported.', call, 'auth_config.strategies.siwe');
|
|
1315
|
+
const domain = (0, upgrade_1.propOf)(o, 'domain');
|
|
1316
|
+
if (domain) {
|
|
1317
|
+
const d = b.configString(domain, 'SIWE domain', 'SIWE_DOMAIN', { area, option: 'siwe.domain' });
|
|
1318
|
+
b.doc.oauth_providers.push({ provider: 'siwe', settings: { domain: d } });
|
|
1319
|
+
b.add('mapped', area, 'siwe.domain', 'Message domain kept.', domain, 'oauth_providers siwe settings.domain');
|
|
1320
|
+
}
|
|
1321
|
+
if ((0, upgrade_1.propOf)(o, 'ensLookup'))
|
|
1322
|
+
b.add('mapped', area, 'siwe.ensLookup', 'Atlas resolves ENS names and avatars for wallet users.', (0, upgrade_1.propOf)(o, 'ensLookup'));
|
|
1323
|
+
if ((0, upgrade_1.propOf)(o, 'getNonce') || (0, upgrade_1.propOf)(o, 'verifyMessage'))
|
|
1324
|
+
b.add('mapped', area, 'siwe.verifyMessage', 'Atlas issues nonces and verifies signatures itself (incl. smart-contract wallets).', (0, upgrade_1.propOf)(o, 'getNonce') ?? (0, upgrade_1.propOf)(o, 'verifyMessage'));
|
|
1325
|
+
if ((0, upgrade_1.propOf)(o, 'anonymous') !== undefined && bool((0, upgrade_1.propOf)(o, 'anonymous')) === false)
|
|
1326
|
+
b.add('differs', area, 'siwe.anonymous', 'Atlas lets wallet users sign up without an email; require one with an identity trait if needed.', (0, upgrade_1.propOf)(o, 'anonymous'));
|
|
1327
|
+
},
|
|
1328
|
+
captcha(b, { options: o, call }) {
|
|
1329
|
+
const area = 'plugin:captcha';
|
|
1330
|
+
const provider = str((0, upgrade_1.propOf)(o, 'provider'));
|
|
1331
|
+
let atlasProvider = provider ? CAPTCHA_PROVIDERS[provider] : undefined;
|
|
1332
|
+
const minScore = num((0, upgrade_1.propOf)(o, 'minScore'));
|
|
1333
|
+
if (provider === 'google-recaptcha' && minScore !== undefined)
|
|
1334
|
+
atlasProvider = 'recaptcha_v3';
|
|
1335
|
+
if (!atlasProvider) {
|
|
1336
|
+
b.add('manual', area, 'captcha.provider', `Unknown captcha provider "${provider ?? describe((0, upgrade_1.propOf)(o, 'provider') ?? call)}". Atlas supports Turnstile, hCaptcha, reCAPTCHA v2/v3/Enterprise, CaptchaFox, Vercel BotID and more.`, (0, upgrade_1.propOf)(o, 'provider') ?? call);
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
const captcha = { provider: atlasProvider };
|
|
1340
|
+
const siteKey = (0, upgrade_1.propOf)(o, 'siteKey');
|
|
1341
|
+
if (atlasProvider !== 'vercel_botid') {
|
|
1342
|
+
captcha.siteKey = siteKey ? b.configString(siteKey, 'captcha site key', 'CAPTCHA_SITE_KEY', { area, option: 'captcha.siteKey' }) : '${CAPTCHA_SITE_KEY}';
|
|
1343
|
+
if (!siteKey) {
|
|
1344
|
+
b.env.set('CAPTCHA_SITE_KEY', 'captcha site key (the public key your sign-in form used)');
|
|
1345
|
+
b.add('manual', area, 'captcha.siteKey', 'Better Auth only knows the secret; Atlas renders the widget itself, so set CAPTCHA_SITE_KEY to your public site key.', call);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
if (minScore !== undefined)
|
|
1349
|
+
captcha.minScore = minScore;
|
|
1350
|
+
const verify = (0, upgrade_1.propOf)(o, 'siteVerifyURLOverride');
|
|
1351
|
+
if (verify)
|
|
1352
|
+
captcha.verifyUrl = b.configString(verify, 'captcha verify URL', 'CAPTCHA_VERIFY_URL', { area, option: 'captcha.siteVerifyURLOverride' });
|
|
1353
|
+
const endpoints = (0, upgrade_1.propOf)(o, 'endpoints');
|
|
1354
|
+
const paths = endpoints?.kind === 'array' ? endpoints.items.map(str).filter((s) => Boolean(s)) : ['/sign-up/email', '/sign-in/email', '/forget-password', '/request-password-reset'];
|
|
1355
|
+
const flows = {};
|
|
1356
|
+
for (const p of paths) {
|
|
1357
|
+
if (p.startsWith('/sign-up'))
|
|
1358
|
+
flows.signUp = { enabled: true };
|
|
1359
|
+
else if (p.startsWith('/sign-in'))
|
|
1360
|
+
flows.signIn = { enabled: true };
|
|
1361
|
+
else if (/forget-password|request-password-reset|reset-password/.test(p))
|
|
1362
|
+
flows.passwordReset = { enabled: true };
|
|
1363
|
+
else if (/anonymous/.test(p))
|
|
1364
|
+
flows.guest = { enabled: true };
|
|
1365
|
+
}
|
|
1366
|
+
captcha.flows = flows;
|
|
1367
|
+
b.set('captcha', captcha);
|
|
1368
|
+
b.add('mapped', area, 'captcha()', `${atlasProvider} on ${Object.keys(flows).join(', ') || 'no flows'}.`, call, 'auth_config.captcha');
|
|
1369
|
+
const secret = (0, upgrade_1.propOf)(o, 'secretKey');
|
|
1370
|
+
if (secret?.kind === 'env') {
|
|
1371
|
+
b.secrets.push({ kind: 'captcha_secret', provider: atlasProvider, env: secret.name });
|
|
1372
|
+
b.add('mapped', area, 'captcha.secretKey', `Stored write-only from ${secret.name} before the captcha is turned on.`, secret, 'PUT /v1/instance/captcha_secret');
|
|
1373
|
+
}
|
|
1374
|
+
else {
|
|
1375
|
+
b.secrets.push({ kind: 'captcha_secret', provider: atlasProvider, env: 'CAPTCHA_SECRET_KEY' });
|
|
1376
|
+
b.add('manual', area, 'captcha.secretKey', secret?.kind === 'string' ? 'The secret is hard-coded in source; it was not copied. Set CAPTCHA_SECRET_KEY before applying.' : 'Set CAPTCHA_SECRET_KEY before applying; it is stored write-only.', secret ?? call, 'PUT /v1/instance/captcha_secret');
|
|
1377
|
+
}
|
|
1378
|
+
},
|
|
1379
|
+
haveIBeenPwned(b, { options: o, call }) {
|
|
1380
|
+
b.set('passwordPolicy.checkBreaches', true);
|
|
1381
|
+
b.add('mapped', 'plugin:haveIBeenPwned', 'haveIBeenPwned()', 'Breached passwords are refused (k-anonymity range check).', call, 'auth_config.passwordPolicy.checkBreaches');
|
|
1382
|
+
if ((0, upgrade_1.propOf)(o, 'customPasswordCompromisedMessage'))
|
|
1383
|
+
b.add('differs', 'plugin:haveIBeenPwned', 'haveIBeenPwned.customPasswordCompromisedMessage', 'Change the message through localization overrides.', (0, upgrade_1.propOf)(o, 'customPasswordCompromisedMessage'));
|
|
1384
|
+
},
|
|
1385
|
+
customSession(b, { call }) {
|
|
1386
|
+
b.add('manual', 'plugin:customSession', 'customSession()', 'Extra session fields become claims in the "session" JWT template (from user fields and metadata), or session custom data set by a post-login Action. Clients read them with useSession() / auth().', call, 'jwt_templates session');
|
|
1387
|
+
},
|
|
1388
|
+
genericOAuth(b, { options: o, call }) {
|
|
1389
|
+
translateGenericOAuth(b, o, call);
|
|
1390
|
+
},
|
|
1391
|
+
sso(b, { options: o, call }) {
|
|
1392
|
+
const area = 'plugin:sso';
|
|
1393
|
+
b.add('mapped', area, 'sso()', 'Enterprise SSO (SAML 2.0 and OIDC) per organization, with domain verification and JIT provisioning.', call, 'sso_connections');
|
|
1394
|
+
b.add('manual', area, 'sso providers', 'SSO connections registered at runtime live in your database (ssoProvider). Recreate each one in Atlas (dashboard, Backend API or the sso_connections config file) on its organization, and ask the IdP admin to add the Atlas ACS / redirect URL.', call, 'sso_connections');
|
|
1395
|
+
const defaults = (0, upgrade_1.propOf)(o, 'defaultSSO');
|
|
1396
|
+
if (defaults?.kind === 'array') {
|
|
1397
|
+
for (const d of defaults.items) {
|
|
1398
|
+
const domain = str((0, upgrade_1.propOf)(d, 'domain'));
|
|
1399
|
+
const pid = str((0, upgrade_1.propOf)(d, 'providerId'));
|
|
1400
|
+
b.add('manual', area, `sso.defaultSSO ${pid ?? domain ?? ''}`.trim(), `Create this ${(0, upgrade_1.propOf)(d, 'samlConfig') ? 'SAML' : 'OIDC'} connection in Atlas${domain ? ` with allowed domain ${domain}` : ''} and attach it to its organization.`, d, 'sso_connections');
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
if ((0, upgrade_1.propOf)(o, 'provisionUser') || (0, upgrade_1.propOf)(o, 'organizationProvisioning'))
|
|
1404
|
+
b.add('mapped', area, 'sso.provisionUser', 'JIT provisioning and organization seating are per connection (default role, claim → role mapping); custom logic goes to a post-login Action.', (0, upgrade_1.propOf)(o, 'provisionUser') ?? (0, upgrade_1.propOf)(o, 'organizationProvisioning'));
|
|
1405
|
+
if ((0, upgrade_1.propOf)(o, 'domainVerification'))
|
|
1406
|
+
b.add('mapped', area, 'sso.domainVerification', 'Organizations verify their domains in Atlas (DNS TXT).', (0, upgrade_1.propOf)(o, 'domainVerification'));
|
|
1407
|
+
if (bool((0, upgrade_1.propOf)(o, 'trustEmailVerified')))
|
|
1408
|
+
b.add('differs', area, 'sso.trustEmailVerified', 'Atlas treats emails on a verified domain of the connection as verified.', (0, upgrade_1.propOf)(o, 'trustEmailVerified'));
|
|
1409
|
+
},
|
|
1410
|
+
oidcProvider(b, { options: o, call }) {
|
|
1411
|
+
translateOAuthServer(b, o, call, 'oidcProvider');
|
|
1412
|
+
},
|
|
1413
|
+
oauthProvider(b, { options: o, call }) {
|
|
1414
|
+
translateOAuthServer(b, o, call, 'oauthProvider');
|
|
1415
|
+
},
|
|
1416
|
+
mcp(b, { options: o, call }) {
|
|
1417
|
+
const area = 'plugin:mcp';
|
|
1418
|
+
b.set('oauthProvider.dynamicRegistration', true);
|
|
1419
|
+
b.set('oauthProvider.requirePkce', true);
|
|
1420
|
+
b.add('mapped', area, 'mcp()', 'MCP clients register dynamically and authorize with PKCE against Atlas as the OAuth server; your MCP server verifies Atlas access tokens (protected-resource metadata points at Atlas).', call, 'auth_config.oauthProvider');
|
|
1421
|
+
const resource = (0, upgrade_1.propOf)(o, 'resource');
|
|
1422
|
+
if (resource) {
|
|
1423
|
+
const id = b.configString(resource, 'MCP resource identifier', 'MCP_RESOURCE_URL', { area, option: 'mcp.resource' });
|
|
1424
|
+
b.doc.resource_servers.push({ identifier: id, name: 'MCP server', scopes: ['openid', 'profile', 'email', 'offline_access'] });
|
|
1425
|
+
b.add('mapped', area, 'mcp.resource', 'The MCP server is an API (resource server) Atlas issues tokens for.', resource, 'resource_servers');
|
|
1426
|
+
}
|
|
1427
|
+
if ((0, upgrade_1.propOf)(o, 'loginPage'))
|
|
1428
|
+
b.add('mapped', area, 'mcp.loginPage', 'The hosted Atlas sign-in and consent pages are used.', (0, upgrade_1.propOf)(o, 'loginPage'));
|
|
1429
|
+
if ((0, upgrade_1.propOf)(o, 'oidcConfig'))
|
|
1430
|
+
translateOAuthServer(b, (0, upgrade_1.propOf)(o, 'oidcConfig'), call, 'mcp.oidcConfig');
|
|
1431
|
+
},
|
|
1432
|
+
deviceAuthorization(b, { options: o, call }) {
|
|
1433
|
+
const area = 'plugin:deviceAuthorization';
|
|
1434
|
+
b.add('mapped', area, 'deviceAuthorization()', 'The device authorization grant (RFC 8628) is part of Atlas as an OAuth server: give each device app the device_code grant on its OAuth client.', call, 'oauth_clients grant_types');
|
|
1435
|
+
const exp = durationSeconds((0, upgrade_1.propOf)(o, 'expiresIn'));
|
|
1436
|
+
if (exp !== undefined) {
|
|
1437
|
+
b.set('oauthProvider.tokenLifetimes.deviceCodeSeconds', exp);
|
|
1438
|
+
b.add('mapped', area, 'deviceAuthorization.expiresIn', `Device codes expire after ${humanDuration(exp)}.`, (0, upgrade_1.propOf)(o, 'expiresIn'), 'auth_config.oauthProvider.tokenLifetimes.deviceCodeSeconds');
|
|
1439
|
+
}
|
|
1440
|
+
for (const k of ['interval', 'userCodeLength', 'deviceCodeLength', 'generateDeviceCode', 'generateUserCode']) {
|
|
1441
|
+
if ((0, upgrade_1.propOf)(o, k))
|
|
1442
|
+
b.add('differs', area, `deviceAuthorization.${k}`, 'Atlas uses its own polling interval and code format.', (0, upgrade_1.propOf)(o, k));
|
|
1443
|
+
}
|
|
1444
|
+
if ((0, upgrade_1.propOf)(o, 'validateClient'))
|
|
1445
|
+
b.add('mapped', area, 'deviceAuthorization.validateClient', 'Only registered OAuth clients with the device_code grant may start the flow.', (0, upgrade_1.propOf)(o, 'validateClient'));
|
|
1446
|
+
if ((0, upgrade_1.propOf)(o, 'verificationUri'))
|
|
1447
|
+
b.add('differs', area, 'deviceAuthorization.verificationUri', 'The verification page is hosted by Atlas.', (0, upgrade_1.propOf)(o, 'verificationUri'));
|
|
1448
|
+
},
|
|
1449
|
+
stripe(b, p) {
|
|
1450
|
+
translatePayments(b, { ...p, id: 'payments:stripe' });
|
|
1451
|
+
},
|
|
1452
|
+
lastLoginMethod(b, { call }) {
|
|
1453
|
+
b.set('accountUx.lastSignInMethod.enabled', true);
|
|
1454
|
+
b.add('mapped', 'plugin:lastLoginMethod', 'lastLoginMethod()', 'The sign-in page highlights the method used last.', call, 'auth_config.accountUx.lastSignInMethod');
|
|
1455
|
+
},
|
|
1456
|
+
nextCookies(b, { call }) {
|
|
1457
|
+
b.add('mapped', 'plugin:nextCookies', 'nextCookies()', 'Not needed: the Atlas Next.js SDK sets cookies in server actions itself.', call);
|
|
1458
|
+
},
|
|
1459
|
+
openAPI(b, { call }) {
|
|
1460
|
+
b.add('mapped', 'plugin:openAPI', 'openAPI()', 'Atlas publishes its own OpenAPI document for every endpoint.', call);
|
|
1461
|
+
},
|
|
1462
|
+
oneTimeToken(b, { call }) {
|
|
1463
|
+
b.add('differs', 'plugin:oneTimeToken', 'oneTimeToken()', 'Use sign-in tokens (POST /v1/sign_in_tokens) to hand a session to another device or app.', call);
|
|
1464
|
+
},
|
|
1465
|
+
oAuthProxy(b, { call }) {
|
|
1466
|
+
b.add('mapped', 'plugin:oAuthProxy', 'oAuthProxy()', 'Not needed: provider callbacks land on Atlas, so preview deployments work once their origins are allowed (a wildcard under your domain).', call);
|
|
1467
|
+
},
|
|
1468
|
+
expo(b, { call }) {
|
|
1469
|
+
b.add('manual', 'plugin:expo', 'expo()', 'Use @atlasauth/expo; add the app scheme as a redirect URL and the app under auth_config.nativeApps.', call, 'redirect_urls / auth_config.nativeApps');
|
|
1470
|
+
},
|
|
1471
|
+
scim(b, { call }) {
|
|
1472
|
+
b.add('mapped', 'plugin:scim', 'scim()', 'SCIM 2.0 provisioning is built in per organization connection; issue a SCIM token for each directory.', call);
|
|
1473
|
+
},
|
|
1474
|
+
electron(b, { call }) {
|
|
1475
|
+
b.add('manual', 'plugin:electron', 'electron()', 'Use @atlasauth/electron (system-browser sign-in with PKCE).', call);
|
|
1476
|
+
},
|
|
1477
|
+
i18n(b, { call }) {
|
|
1478
|
+
b.add('differs', 'plugin:i18n', 'i18n()', 'Atlas localises hosted pages and emails (auth_config.localization and localization overrides).', call, 'auth_config.localization');
|
|
1479
|
+
},
|
|
1480
|
+
};
|
|
1481
|
+
PLUGIN_HANDLERS.haveibeenpwned = PLUGIN_HANDLERS.haveIBeenPwned;
|
|
1482
|
+
PLUGIN_HANDLERS.api_key = PLUGIN_HANDLERS.apiKey;
|
|
1483
|
+
// ───────────────────────────── organization ─────────────────────────────
|
|
1484
|
+
function translateOrganization(b, o, call) {
|
|
1485
|
+
const area = 'plugin:organization';
|
|
1486
|
+
b.add('mapped', area, 'organization()', 'Organizations, members, invitations and roles; organizations and memberships are imported.', call);
|
|
1487
|
+
const allowCreate = (0, upgrade_1.propOf)(o, 'allowUserToCreateOrganization');
|
|
1488
|
+
if (allowCreate) {
|
|
1489
|
+
if (bool(allowCreate) === false)
|
|
1490
|
+
b.add('manual', area, 'organization.allowUserToCreateOrganization', 'To stop users creating organizations, refuse it in a pre-organization-create Action (or create organizations only from your backend).', allowCreate);
|
|
1491
|
+
else if (isFn(allowCreate))
|
|
1492
|
+
b.add('manual', area, 'organization.allowUserToCreateOrganization', 'Move this check to a pre-organization-create Action.', allowCreate);
|
|
1493
|
+
}
|
|
1494
|
+
const limit = (0, upgrade_1.propOf)(o, 'organizationLimit');
|
|
1495
|
+
if (limit) {
|
|
1496
|
+
const n = num(limit);
|
|
1497
|
+
if (n !== undefined) {
|
|
1498
|
+
b.set('organizations.maxOrganizationsPerUser', clamp(n, 0, 1_000_000));
|
|
1499
|
+
b.add('mapped', area, 'organization.organizationLimit', `${n} organizations per user.`, limit, 'auth_config.organizations.maxOrganizationsPerUser');
|
|
1500
|
+
}
|
|
1501
|
+
else
|
|
1502
|
+
b.add('manual', area, 'organization.organizationLimit', 'A function decides the limit; enforce it in a pre-organization-create Action.', limit);
|
|
1503
|
+
}
|
|
1504
|
+
const members = (0, upgrade_1.propOf)(o, 'membershipLimit');
|
|
1505
|
+
const membersN = num(members) ?? (members ? undefined : 100);
|
|
1506
|
+
if (membersN !== undefined) {
|
|
1507
|
+
b.set('organizations.defaultMaxAllowedMemberships', clamp(membersN, 0, 1_000_000));
|
|
1508
|
+
b.add('mapped', area, 'organization.membershipLimit', `${membersN} members per organization${members ? '' : ' (the Better Auth default)'}.`, members ?? call, 'auth_config.organizations.defaultMaxAllowedMemberships');
|
|
1509
|
+
}
|
|
1510
|
+
else if (members)
|
|
1511
|
+
b.add('manual', area, 'organization.membershipLimit', 'A function decides the limit; set per-organization max_allowed_memberships or enforce it in a pre-member-add Action.', members);
|
|
1512
|
+
const invTtl = num((0, upgrade_1.propOf)(o, 'invitationExpiresIn')) ?? 48 * 3600;
|
|
1513
|
+
const ttl = clamp(invTtl, 3600, 90 * DAY);
|
|
1514
|
+
b.set('organizations.invitationTtlSeconds', ttl);
|
|
1515
|
+
b.add(ttl === invTtl ? 'mapped' : 'differs', area, 'organization.invitationExpiresIn', `Invitations expire after ${humanDuration(ttl)}${ttl !== invTtl ? ' (Atlas allows 1 hour to 90 days)' : ''}.`, (0, upgrade_1.propOf)(o, 'invitationExpiresIn') ?? call, 'auth_config.organizations.invitationTtlSeconds');
|
|
1516
|
+
const invLimit = (0, upgrade_1.propOf)(o, 'invitationLimit');
|
|
1517
|
+
const invN = num(invLimit) ?? (invLimit ? undefined : 100);
|
|
1518
|
+
if (invN !== undefined) {
|
|
1519
|
+
b.set('organizations.maxPendingInvitationsPerOrganization', clamp(invN, 0, 1_000_000));
|
|
1520
|
+
b.add('mapped', area, 'organization.invitationLimit', `${invN} pending invitations per organization.`, invLimit ?? call, 'auth_config.organizations.maxPendingInvitationsPerOrganization');
|
|
1521
|
+
}
|
|
1522
|
+
else if (invLimit)
|
|
1523
|
+
b.add('manual', area, 'organization.invitationLimit', 'A function decides the limit; enforce it in a pre-invitation-create Action.', invLimit);
|
|
1524
|
+
if ((0, upgrade_1.propOf)(o, 'sendInvitationEmail'))
|
|
1525
|
+
b.add('differs', area, 'organization.sendInvitationEmail', 'Atlas sends invitation emails itself (customise the invitation template); the accept link goes to the hosted page or your app.', (0, upgrade_1.propOf)(o, 'sendInvitationEmail'));
|
|
1526
|
+
if ((0, upgrade_1.propOf)(o, 'requireEmailVerificationOnInvitation'))
|
|
1527
|
+
b.add('mapped', area, 'organization.requireEmailVerificationOnInvitation', 'Accepting an invitation always needs a proven email matching the invitation.', (0, upgrade_1.propOf)(o, 'requireEmailVerificationOnInvitation'));
|
|
1528
|
+
if ((0, upgrade_1.propOf)(o, 'cancelPendingInvitationsOnReInvite'))
|
|
1529
|
+
b.add('differs', area, 'organization.cancelPendingInvitationsOnReInvite', 'Re-inviting an address refreshes its pending invitation.', (0, upgrade_1.propOf)(o, 'cancelPendingInvitationsOnReInvite'));
|
|
1530
|
+
if (bool((0, upgrade_1.propOf)(o, 'disableOrganizationDeletion'))) {
|
|
1531
|
+
b.set('organizations.allowDeletionFromApp', false);
|
|
1532
|
+
b.add('mapped', area, 'organization.disableOrganizationDeletion', 'Organizations cannot be deleted from the app (the Backend API still can).', (0, upgrade_1.propOf)(o, 'disableOrganizationDeletion'), 'auth_config.organizations.allowDeletionFromApp');
|
|
1533
|
+
}
|
|
1534
|
+
const hooks = (0, upgrade_1.propOf)(o, 'organizationHooks') ?? (0, upgrade_1.propOf)(o, 'organizationCreation');
|
|
1535
|
+
if (hooks?.kind === 'object') {
|
|
1536
|
+
for (const [name, fn] of Object.entries(hooks.props)) {
|
|
1537
|
+
const target = /^before(Create|CreateOrganization)/.test(name)
|
|
1538
|
+
? 'a pre-organization-create Action'
|
|
1539
|
+
: /^before(AddMember|CreateInvitation|AcceptInvitation)/.test(name)
|
|
1540
|
+
? name.includes('Invitation') ? 'a pre-invitation-create Action' : 'a pre-member-add Action'
|
|
1541
|
+
: 'a webhook (organization.*, organizationMembership.*, organizationInvitation.* events)';
|
|
1542
|
+
b.add('manual', area, `organization.${hooks === (0, upgrade_1.propOf)(o, 'organizationHooks') ? 'organizationHooks' : 'organizationCreation'}.${name}`, `Move this to ${target}.`, fn);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
else if (hooks)
|
|
1546
|
+
b.add('manual', area, 'organization.organizationHooks', 'Move these hooks to Actions (pre-organization-create, pre-member-add, pre-invitation-create) and webhooks.', hooks);
|
|
1547
|
+
const teams = (0, upgrade_1.propOf)(o, 'teams');
|
|
1548
|
+
if (teams && bool((0, upgrade_1.propOf)(teams, 'enabled')) !== false) {
|
|
1549
|
+
b.add('mapped', area, 'organization.teams', 'Teams inside organizations; teams and team members are imported.', teams);
|
|
1550
|
+
const maxTeams = num((0, upgrade_1.propOf)(teams, 'maximumTeams'));
|
|
1551
|
+
if (maxTeams !== undefined) {
|
|
1552
|
+
b.set('organizations.maxTeamsPerOrganization', clamp(maxTeams, 0, 1_000_000));
|
|
1553
|
+
b.add('mapped', area, 'organization.teams.maximumTeams', `${maxTeams} teams per organization.`, (0, upgrade_1.propOf)(teams, 'maximumTeams'), 'auth_config.organizations.maxTeamsPerOrganization');
|
|
1554
|
+
}
|
|
1555
|
+
else if ((0, upgrade_1.propOf)(teams, 'maximumTeams'))
|
|
1556
|
+
b.add('manual', area, 'organization.teams.maximumTeams', 'A function decides the limit; review it.', (0, upgrade_1.propOf)(teams, 'maximumTeams'));
|
|
1557
|
+
const maxMembers = num((0, upgrade_1.propOf)(teams, 'maximumMembersPerTeam'));
|
|
1558
|
+
if (maxMembers !== undefined) {
|
|
1559
|
+
b.set('organizations.maxMembersPerTeam', clamp(maxMembers, 0, 1_000_000));
|
|
1560
|
+
b.add('mapped', area, 'organization.teams.maximumMembersPerTeam', `${maxMembers} members per team.`, (0, upgrade_1.propOf)(teams, 'maximumMembersPerTeam'), 'auth_config.organizations.maxMembersPerTeam');
|
|
1561
|
+
}
|
|
1562
|
+
if ((0, upgrade_1.propOf)(teams, 'defaultTeam'))
|
|
1563
|
+
b.add('manual', area, 'organization.teams.defaultTeam', 'Create the default team from a webhook on organization.created.', (0, upgrade_1.propOf)(teams, 'defaultTeam'));
|
|
1564
|
+
if ((0, upgrade_1.propOf)(teams, 'allowRemovingAllTeams'))
|
|
1565
|
+
b.add('mapped', area, 'organization.teams.allowRemovingAllTeams', 'An organization may have no teams.', (0, upgrade_1.propOf)(teams, 'allowRemovingAllTeams'));
|
|
1566
|
+
}
|
|
1567
|
+
const dac = (0, upgrade_1.propOf)(o, 'dynamicAccessControl');
|
|
1568
|
+
if (dac) {
|
|
1569
|
+
const on = bool((0, upgrade_1.propOf)(dac, 'enabled')) === true;
|
|
1570
|
+
b.set('organizations.allowCustomRolesFromApp', on);
|
|
1571
|
+
b.add('mapped', area, 'organization.dynamicAccessControl', on ? 'Organization admins can create their own roles from the app.' : 'Custom roles only from the dashboard / Backend API.', dac, 'auth_config.organizations.allowCustomRolesFromApp');
|
|
1572
|
+
const maxRoles = num((0, upgrade_1.propOf)(dac, 'maximumRolesPerOrganization'));
|
|
1573
|
+
if (maxRoles !== undefined) {
|
|
1574
|
+
b.set('organizations.maxCustomRolesPerOrganization', clamp(maxRoles, 0, 1_000_000));
|
|
1575
|
+
b.add('mapped', area, 'organization.dynamicAccessControl.maximumRolesPerOrganization', `${maxRoles} custom roles per organization.`, (0, upgrade_1.propOf)(dac, 'maximumRolesPerOrganization'), 'auth_config.organizations.maxCustomRolesPerOrganization');
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
else {
|
|
1579
|
+
b.set('organizations.allowCustomRolesFromApp', false);
|
|
1580
|
+
}
|
|
1581
|
+
// Roles: built-ins map onto the Atlas system roles; custom ones are created.
|
|
1582
|
+
const creator = str((0, upgrade_1.propOf)(o, 'creatorRole')) ?? 'owner';
|
|
1583
|
+
const roles = (0, upgrade_1.propOf)(o, 'roles');
|
|
1584
|
+
const permissions = new Map();
|
|
1585
|
+
const roleDocs = [];
|
|
1586
|
+
const roleKeys = { ...BUILTIN_ORG_ROLES };
|
|
1587
|
+
if (roles?.kind === 'object') {
|
|
1588
|
+
for (const [name, role] of Object.entries(roles.props)) {
|
|
1589
|
+
const statements = statementsOfRole(role);
|
|
1590
|
+
const builtIn = BUILTIN_ORG_ROLES[name];
|
|
1591
|
+
if (builtIn && statements === null) {
|
|
1592
|
+
roleKeys[name] = builtIn;
|
|
1593
|
+
continue;
|
|
1594
|
+
}
|
|
1595
|
+
if (statements === null) {
|
|
1596
|
+
b.add('manual', area, `organization.roles.${name}`, `Cannot read this role's permissions (${describe(role)}); create the role by hand.`, role, 'roles');
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
1599
|
+
const key = builtIn && name !== 'admin' ? builtIn : `org:${keySegment(name === 'admin' ? 'admin_ba' : name)}`;
|
|
1600
|
+
if (builtIn && name !== 'admin') {
|
|
1601
|
+
roleKeys[name] = builtIn;
|
|
1602
|
+
b.add('differs', area, `organization.roles.${name}`, `Mapped to the Atlas system role ${builtIn}, whose permissions are fixed; your customised statements for "${name}" are not applied (create a custom role instead if they matter).`, role, `roles ${builtIn}`);
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
roleKeys[name] = key;
|
|
1606
|
+
const perms = [];
|
|
1607
|
+
for (const [resource, actions] of Object.entries(statements)) {
|
|
1608
|
+
for (const action of actions) {
|
|
1609
|
+
const sys = BUILTIN_STATEMENT_PERMS[resource]?.[action];
|
|
1610
|
+
if (sys) {
|
|
1611
|
+
if (!perms.includes(sys))
|
|
1612
|
+
perms.push(sys);
|
|
1613
|
+
continue;
|
|
1614
|
+
}
|
|
1615
|
+
if (BUILTIN_STATEMENT_PERMS[resource])
|
|
1616
|
+
continue; // e.g. ac:create → dashboard-managed
|
|
1617
|
+
const pkey = `org:${keySegment(resource)}:${keySegment(action)}`;
|
|
1618
|
+
permissions.set(pkey, `${resource}: ${action}`);
|
|
1619
|
+
if (!perms.includes(pkey))
|
|
1620
|
+
perms.push(pkey);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
roleDocs.push({ key, name: titleCase(name), description: `Migrated from the Better Auth "${name}" role.`, permissions: perms.sort() });
|
|
1624
|
+
b.add('mapped', area, `organization.roles.${name}`, `Custom role ${key} with ${perms.length} permission(s).`, role, `roles ${key}`);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
else if (roles) {
|
|
1628
|
+
b.add('manual', area, 'organization.roles', `Cannot read the roles (${describe(roles)}); create them by hand.`, roles);
|
|
1629
|
+
}
|
|
1630
|
+
if ((0, upgrade_1.propOf)(o, 'ac'))
|
|
1631
|
+
b.add('mapped', area, 'organization.ac', 'Every statement became a permission (org:<resource>:<action>); Better Auth\'s built-in organization/member/invitation/team statements map onto Atlas system permissions.', (0, upgrade_1.propOf)(o, 'ac'), 'permissions');
|
|
1632
|
+
b.doc.permissions.push(...[...permissions.entries()].sort(([a], [c]) => (a < c ? -1 : 1)).map(([key, description]) => ({ key, name: key.split(':').slice(1).join(' '), description })));
|
|
1633
|
+
b.doc.roles.push(...roleDocs);
|
|
1634
|
+
const creatorKey = roleKeys[creator] ?? `org:${keySegment(creator)}`;
|
|
1635
|
+
b.set('organizations.creatorRole', creatorKey);
|
|
1636
|
+
b.add(creator === 'owner' || creator === 'admin' ? 'mapped' : 'differs', area, 'organization.creatorRole', `The creator is seated as ${creatorKey}.`, (0, upgrade_1.propOf)(o, 'creatorRole') ?? call, 'auth_config.organizations.creatorRole');
|
|
1637
|
+
Object.assign(b.roles, roleKeys);
|
|
1638
|
+
b.add('differs', area, 'organization (owner role)', 'Better Auth "owner" and "admin" both map to the Atlas org:admin role (which can also delete the organization). A custom "admin" role with its own statements becomes org:admin_ba.', call, 'roles');
|
|
1639
|
+
}
|
|
1640
|
+
function titleCase(s) {
|
|
1641
|
+
return s.replace(/[-_]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1642
|
+
}
|
|
1643
|
+
/** `ac.newRole({ project: ['create'] })` → { project: ['create'] }; null when unreadable. */
|
|
1644
|
+
function statementsOfRole(role) {
|
|
1645
|
+
let stmts;
|
|
1646
|
+
if (role.kind === 'call' && /(^|\.)newRole$/.test(role.callee))
|
|
1647
|
+
stmts = role.args[0];
|
|
1648
|
+
else if (role.kind === 'object' && (0, upgrade_1.propOf)(role, 'statements'))
|
|
1649
|
+
stmts = (0, upgrade_1.propOf)(role, 'statements');
|
|
1650
|
+
if (!stmts || stmts.kind !== 'object')
|
|
1651
|
+
return null;
|
|
1652
|
+
const out = {};
|
|
1653
|
+
for (const [resource, actions] of Object.entries(stmts.props)) {
|
|
1654
|
+
if (actions.kind !== 'array')
|
|
1655
|
+
return null;
|
|
1656
|
+
out[resource] = actions.items.map(str).filter((s) => Boolean(s));
|
|
1657
|
+
}
|
|
1658
|
+
if (stmts.unresolvedSpreads.length)
|
|
1659
|
+
return null;
|
|
1660
|
+
return out;
|
|
1661
|
+
}
|
|
1662
|
+
// ───────────────────────────── generic OAuth ─────────────────────────────
|
|
1663
|
+
function translateGenericOAuth(b, o, call) {
|
|
1664
|
+
const area = 'plugin:genericOAuth';
|
|
1665
|
+
const configs = (0, upgrade_1.propOf)(o, 'config');
|
|
1666
|
+
if (configs?.kind !== 'array') {
|
|
1667
|
+
b.add('manual', area, 'genericOAuth.config', `Cannot read the provider list (${configs ? describe(configs) : 'missing'}).`, configs ?? call);
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
const keys = b.get('strategies.oauthProviders') ?? [];
|
|
1671
|
+
for (const entry of configs.items) {
|
|
1672
|
+
// Better Auth presets: okta({...}), auth0({...}), keycloak({...}), microsoftEntraId({...}) …
|
|
1673
|
+
let cfg = entry;
|
|
1674
|
+
let preset;
|
|
1675
|
+
if (entry.kind === 'call') {
|
|
1676
|
+
preset = entry.imported?.name ?? entry.callee;
|
|
1677
|
+
cfg = entry.args[0];
|
|
1678
|
+
}
|
|
1679
|
+
const providerId = str((0, upgrade_1.propOf)(cfg, 'providerId')) ?? preset;
|
|
1680
|
+
if (!providerId || !cfg) {
|
|
1681
|
+
b.add('manual', area, 'genericOAuth.config[]', `Cannot read this provider (${describe(entry)}).`, entry);
|
|
1682
|
+
continue;
|
|
1683
|
+
}
|
|
1684
|
+
const option = `genericOAuth.config["${providerId}"]`;
|
|
1685
|
+
const upper = slug(providerId).toUpperCase();
|
|
1686
|
+
const clientId = b.configString((0, upgrade_1.propOf)(cfg, 'clientId'), `${providerId} client id`, `${upper}_CLIENT_ID`, { area, option: `${option}.clientId` });
|
|
1687
|
+
const clientSecret = b.configString((0, upgrade_1.propOf)(cfg, 'clientSecret'), `${providerId} client secret`, `${upper}_CLIENT_SECRET`, { area, option: `${option}.clientSecret`, secret: true });
|
|
1688
|
+
const scopes = (0, upgrade_1.propOf)(cfg, 'scopes');
|
|
1689
|
+
const scopeList = scopes?.kind === 'array' ? scopes.items.map(str).filter((s) => Boolean(s)) : undefined;
|
|
1690
|
+
const presetKey = preset ? GENERIC_PRESETS[preset] : undefined;
|
|
1691
|
+
if (presetKey) {
|
|
1692
|
+
b.providers[providerId] = presetKey;
|
|
1693
|
+
const e = { provider: presetKey, enabled: true, allow_sign_in: true, allow_sign_up: true, client_id: clientId, client_secret: clientSecret };
|
|
1694
|
+
const domain = (0, upgrade_1.propOf)(cfg, 'issuer') ?? (0, upgrade_1.propOf)(cfg, 'domain');
|
|
1695
|
+
if (domain && (presetKey === 'okta' || presetKey === 'auth0')) {
|
|
1696
|
+
const d = b.configString(domain, `${presetKey} domain`, `${upper}_DOMAIN`, { area, option: `${option}.issuer` });
|
|
1697
|
+
e.values = { domain: d.replace(/^https?:\/\//, '').replace(/\/.*$/, '') };
|
|
1698
|
+
}
|
|
1699
|
+
if (presetKey === 'microsoft' && (0, upgrade_1.propOf)(cfg, 'tenantId'))
|
|
1700
|
+
e.values = { tenant: b.configString((0, upgrade_1.propOf)(cfg, 'tenantId'), 'Microsoft tenant id', 'MICROSOFT_TENANT_ID', { area, option: `${option}.tenantId` }) };
|
|
1701
|
+
if (scopeList?.length)
|
|
1702
|
+
e.scopes = scopeList;
|
|
1703
|
+
b.doc.oauth_providers.push(e);
|
|
1704
|
+
keys.push(presetKey);
|
|
1705
|
+
b.add('mapped', area, option, `Built-in Atlas provider "${presetKey}". Keep the same OAuth app and add Atlas's callback URL to it.`, entry, `oauth_providers ${presetKey}`);
|
|
1706
|
+
continue;
|
|
1707
|
+
}
|
|
1708
|
+
const key = keySegmentProvider(providerId);
|
|
1709
|
+
b.providers[providerId] = key;
|
|
1710
|
+
const discovery = (0, upgrade_1.propOf)(cfg, 'discoveryUrl');
|
|
1711
|
+
const doc = {
|
|
1712
|
+
provider_key: key,
|
|
1713
|
+
display_name: titleCase(providerId),
|
|
1714
|
+
protocol: discovery ? 'oidc' : 'oauth2',
|
|
1715
|
+
client_id: clientId,
|
|
1716
|
+
client_secret: clientSecret,
|
|
1717
|
+
enabled: true,
|
|
1718
|
+
allow_sign_in: true,
|
|
1719
|
+
allow_sign_up: !(bool((0, upgrade_1.propOf)(cfg, 'disableSignUp')) || bool((0, upgrade_1.propOf)(cfg, 'disableImplicitSignUp'))),
|
|
1720
|
+
};
|
|
1721
|
+
if (discovery) {
|
|
1722
|
+
const url = b.configString(discovery, `${providerId} discovery URL`, `${upper}_DISCOVERY_URL`, { area, option: `${option}.discoveryUrl` });
|
|
1723
|
+
doc.discovery_url = url;
|
|
1724
|
+
const issuer = (0, upgrade_1.propOf)(cfg, 'issuer');
|
|
1725
|
+
doc.issuer = issuer ? b.configString(issuer, `${providerId} issuer`, `${upper}_ISSUER`, { area, option: `${option}.issuer` }) : url.replace(/\/\.well-known\/openid-configuration\/?$/, '');
|
|
1726
|
+
}
|
|
1727
|
+
else {
|
|
1728
|
+
const endpoints = [['authorizationUrl', 'authorization_endpoint'], ['tokenUrl', 'token_endpoint'], ['userInfoUrl', 'userinfo_endpoint']];
|
|
1729
|
+
for (const [baKey, atlasKey] of endpoints) {
|
|
1730
|
+
const v = (0, upgrade_1.propOf)(cfg, baKey);
|
|
1731
|
+
if (v)
|
|
1732
|
+
doc[atlasKey] = b.configString(v, `${providerId} ${baKey}`, `${upper}_${slug(baKey).toUpperCase()}`, { area, option: `${option}.${baKey}` });
|
|
1733
|
+
else if (baKey !== 'userInfoUrl')
|
|
1734
|
+
b.add('manual', area, `${option}.${baKey}`, `Missing ${baKey}; set ${atlasKey} on the custom provider.`, cfg);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
if (scopeList?.length)
|
|
1738
|
+
doc.scopes = scopeList;
|
|
1739
|
+
const pkce = bool((0, upgrade_1.propOf)(cfg, 'pkce'));
|
|
1740
|
+
if (pkce !== undefined)
|
|
1741
|
+
doc.pkce = pkce;
|
|
1742
|
+
const auth = str((0, upgrade_1.propOf)(cfg, 'authentication'));
|
|
1743
|
+
doc.token_endpoint_auth_method = auth === 'basic' ? 'client_secret_basic' : 'client_secret_post';
|
|
1744
|
+
const authParams = (0, upgrade_1.literalOf)((0, upgrade_1.propOf)(cfg, 'authorizationUrlParams'));
|
|
1745
|
+
if (authParams && typeof authParams === 'object')
|
|
1746
|
+
doc.authorization_params = stringMap(authParams);
|
|
1747
|
+
const tokenParams = (0, upgrade_1.literalOf)((0, upgrade_1.propOf)(cfg, 'tokenUrlParams'));
|
|
1748
|
+
if (tokenParams && typeof tokenParams === 'object')
|
|
1749
|
+
doc.token_params = stringMap(tokenParams);
|
|
1750
|
+
for (const k of ['prompt', 'accessType', 'responseMode']) {
|
|
1751
|
+
const v = str((0, upgrade_1.propOf)(cfg, k));
|
|
1752
|
+
if (v) {
|
|
1753
|
+
const params = doc.authorization_params ?? {};
|
|
1754
|
+
params[k === 'accessType' ? 'access_type' : k === 'responseMode' ? 'response_mode' : 'prompt'] = v;
|
|
1755
|
+
doc.authorization_params = params;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
const mapping = profileMappingOf((0, upgrade_1.propOf)(cfg, 'mapProfileToUser'));
|
|
1759
|
+
if (mapping) {
|
|
1760
|
+
doc.profile_mapping = mapping.mapping;
|
|
1761
|
+
b.add(mapping.complete ? 'mapped' : 'differs', area, `${option}.mapProfileToUser`, mapping.complete ? 'Translated into a declarative profile mapping.' : `Partly translated into a profile mapping; check: ${mapping.skipped.join(', ')}.`, (0, upgrade_1.propOf)(cfg, 'mapProfileToUser'), `custom_oauth_providers ${key} profile_mapping`);
|
|
1762
|
+
}
|
|
1763
|
+
else if ((0, upgrade_1.propOf)(cfg, 'mapProfileToUser')) {
|
|
1764
|
+
b.add('manual', area, `${option}.mapProfileToUser`, 'Write the equivalent profile_mapping (claim paths and templates) on the custom provider; preview it with POST /v1/custom_oauth_providers/profile_mapping/preview.', (0, upgrade_1.propOf)(cfg, 'mapProfileToUser'));
|
|
1765
|
+
}
|
|
1766
|
+
if ((0, upgrade_1.propOf)(cfg, 'getUserInfo'))
|
|
1767
|
+
b.add('manual', area, `${option}.getUserInfo`, 'Atlas reads the profile from the userinfo endpoint / id_token (profile_source); custom fetch logic is not supported.', (0, upgrade_1.propOf)(cfg, 'getUserInfo'));
|
|
1768
|
+
if ((0, upgrade_1.propOf)(cfg, 'overrideUserInfo'))
|
|
1769
|
+
doc.profile_mapping = { ...(doc.profile_mapping ?? {}), update_on_sign_in: true };
|
|
1770
|
+
b.doc.custom_oauth_providers.push(doc);
|
|
1771
|
+
keys.push(key);
|
|
1772
|
+
b.add('mapped', area, option, `Custom ${doc.protocol === 'oidc' ? 'OpenID Connect' : 'OAuth 2.0'} provider "${key}". Register its redirect_uri (shown on the provider) at the identity provider.`, entry, `custom_oauth_providers ${key}`);
|
|
1773
|
+
}
|
|
1774
|
+
b.set('strategies.oauthProviders', [...new Set(keys)]);
|
|
1775
|
+
}
|
|
1776
|
+
function keySegmentProvider(id) {
|
|
1777
|
+
let key = slug(id, 48);
|
|
1778
|
+
if (key.length < 2)
|
|
1779
|
+
key = `${key}_oauth`;
|
|
1780
|
+
if (exports.SOCIAL_PROVIDER_MAP[key] || Object.values(exports.SOCIAL_PROVIDER_MAP).includes(key) || GENERIC_PRESETS[key])
|
|
1781
|
+
key = `${key}_custom`;
|
|
1782
|
+
return key.slice(0, 48);
|
|
1783
|
+
}
|
|
1784
|
+
function stringMap(obj) {
|
|
1785
|
+
const out = {};
|
|
1786
|
+
for (const [k, v] of Object.entries(obj))
|
|
1787
|
+
if (v !== null && v !== undefined)
|
|
1788
|
+
out[k] = String(v);
|
|
1789
|
+
return out;
|
|
1790
|
+
}
|
|
1791
|
+
/** `(profile) => ({ email: profile.mail, name: `${profile.first} ${profile.last}` })` → a profile_mapping. */
|
|
1792
|
+
function profileMappingOf(fn) {
|
|
1793
|
+
if (!fn || fn.kind !== 'function' || !fn.returns || fn.returns.kind !== 'object')
|
|
1794
|
+
return null;
|
|
1795
|
+
const param = fn.params[0];
|
|
1796
|
+
const FIELD = {
|
|
1797
|
+
id: 'id',
|
|
1798
|
+
email: 'email',
|
|
1799
|
+
emailVerified: 'email_verified',
|
|
1800
|
+
name: 'full_name',
|
|
1801
|
+
firstName: 'first_name',
|
|
1802
|
+
lastName: 'last_name',
|
|
1803
|
+
image: 'image_url',
|
|
1804
|
+
username: 'username',
|
|
1805
|
+
phoneNumber: 'phone_number',
|
|
1806
|
+
};
|
|
1807
|
+
const fields = {};
|
|
1808
|
+
const publicMeta = {};
|
|
1809
|
+
const skipped = [];
|
|
1810
|
+
const exprOf = (v) => {
|
|
1811
|
+
if (v.kind === 'param' && v.name === param && v.path.length)
|
|
1812
|
+
return v.path.join('.');
|
|
1813
|
+
if (v.kind === 'string' || v.kind === 'number' || v.kind === 'boolean')
|
|
1814
|
+
return { value: v.value };
|
|
1815
|
+
if (v.kind === 'template' && v.parts.every((p) => typeof p === 'string' || (p.kind === 'param' && p.name === param))) {
|
|
1816
|
+
return {
|
|
1817
|
+
template: v.parts.map((p) => (typeof p === 'string' ? p : `{{${p.path.join('.')}}}`)).join(''),
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
if (v.kind === 'unknown' && /\?\?|\|\|/.test(v.text))
|
|
1821
|
+
return null;
|
|
1822
|
+
return null;
|
|
1823
|
+
};
|
|
1824
|
+
for (const [k, v] of Object.entries(fn.returns.props)) {
|
|
1825
|
+
const expr = exprOf(v);
|
|
1826
|
+
if (expr === null) {
|
|
1827
|
+
skipped.push(k);
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
const target = FIELD[k];
|
|
1831
|
+
if (target)
|
|
1832
|
+
fields[target] = expr;
|
|
1833
|
+
else
|
|
1834
|
+
publicMeta[k] = expr;
|
|
1835
|
+
}
|
|
1836
|
+
if (!Object.keys(fields).length && !Object.keys(publicMeta).length)
|
|
1837
|
+
return null;
|
|
1838
|
+
const mapping = {};
|
|
1839
|
+
if (Object.keys(fields).length)
|
|
1840
|
+
mapping.fields = fields;
|
|
1841
|
+
if (Object.keys(publicMeta).length)
|
|
1842
|
+
mapping.public_metadata = publicMeta;
|
|
1843
|
+
return { mapping, complete: skipped.length === 0 && fn.returns.unresolvedSpreads.length === 0, skipped };
|
|
1844
|
+
}
|
|
1845
|
+
// ───────────────────────────── OAuth / OIDC server ─────────────────────────────
|
|
1846
|
+
function translateOAuthServer(b, o, call, name) {
|
|
1847
|
+
const area = `plugin:${name.split('.')[0]}`;
|
|
1848
|
+
b.add('mapped', area, `${name}()`, 'Atlas is an OAuth 2.1 / OpenID Connect provider ("Sign in with Atlas"): clients, consent, discovery, JWKS, introspection and revocation are built in. Clients get new client ids: update each relying party.', call, 'auth_config.oauthProvider');
|
|
1849
|
+
const dcr = (0, upgrade_1.propOf)(o, 'allowDynamicClientRegistration');
|
|
1850
|
+
if (dcr) {
|
|
1851
|
+
b.set('oauthProvider.dynamicRegistration', bool(dcr) === true);
|
|
1852
|
+
b.add('mapped', area, `${name}.allowDynamicClientRegistration`, `Dynamic client registration ${bool(dcr) ? 'on' : 'off'}.`, dcr, 'auth_config.oauthProvider.dynamicRegistration');
|
|
1853
|
+
}
|
|
1854
|
+
const pkce = (0, upgrade_1.propOf)(o, 'requirePKCE') ?? (0, upgrade_1.propOf)(o, 'clientRegistrationRequirePKCE');
|
|
1855
|
+
if (pkce) {
|
|
1856
|
+
b.set('oauthProvider.requirePkce', bool(pkce) !== false);
|
|
1857
|
+
b.add('mapped', area, `${name}.requirePKCE`, 'PKCE required.', pkce, 'auth_config.oauthProvider.requirePkce');
|
|
1858
|
+
}
|
|
1859
|
+
const lifetimes = [
|
|
1860
|
+
['accessTokenExpiresIn', 'accessTokenSeconds'],
|
|
1861
|
+
['idTokenExpiresIn', 'idTokenSeconds'],
|
|
1862
|
+
['refreshTokenExpiresIn', 'refreshTokenAbsoluteSeconds'],
|
|
1863
|
+
['codeExpiresIn', 'authorizationCodeSeconds'],
|
|
1864
|
+
];
|
|
1865
|
+
for (const [baKey, atlasKey] of lifetimes) {
|
|
1866
|
+
const v = durationSeconds((0, upgrade_1.propOf)(o, baKey));
|
|
1867
|
+
if (v === undefined)
|
|
1868
|
+
continue;
|
|
1869
|
+
b.set(`oauthProvider.tokenLifetimes.${atlasKey}`, v);
|
|
1870
|
+
b.add('mapped', area, `${name}.${baKey}`, `${humanDuration(v)}.`, (0, upgrade_1.propOf)(o, baKey), `auth_config.oauthProvider.tokenLifetimes.${atlasKey}`);
|
|
1871
|
+
}
|
|
1872
|
+
const scopes = (0, upgrade_1.propOf)(o, 'scopes');
|
|
1873
|
+
if (scopes?.kind === 'array') {
|
|
1874
|
+
const standard = new Set(['openid', 'profile', 'email', 'offline_access', 'phone', 'address']);
|
|
1875
|
+
const custom = scopes.items.map(str).filter((s) => Boolean(s) && !standard.has(s));
|
|
1876
|
+
if (custom.length) {
|
|
1877
|
+
b.set('oauthProvider.customScopes', custom.map((s) => ({ name: s, description: s, claims: [] })));
|
|
1878
|
+
b.add('mapped', area, `${name}.scopes`, `Custom scopes: ${custom.join(', ')}. Add the claims each scope releases.`, scopes, 'auth_config.oauthProvider.customScopes');
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
const trusted = (0, upgrade_1.propOf)(o, 'trustedClients');
|
|
1882
|
+
if (trusted?.kind === 'array') {
|
|
1883
|
+
for (const c of trusted.items) {
|
|
1884
|
+
const nameV = str((0, upgrade_1.propOf)(c, 'name')) ?? str((0, upgrade_1.propOf)(c, 'clientId')) ?? 'Migrated client';
|
|
1885
|
+
const redirects = (0, upgrade_1.propOf)(c, 'redirectURLs') ?? (0, upgrade_1.propOf)(c, 'redirectUrls') ?? (0, upgrade_1.propOf)(c, 'redirect_uris');
|
|
1886
|
+
const uris = redirects?.kind === 'array' ? redirects.items.map(str).filter((s) => Boolean(s)) : [];
|
|
1887
|
+
const type = str((0, upgrade_1.propOf)(c, 'type'));
|
|
1888
|
+
b.doc.oauth_clients.push({
|
|
1889
|
+
name: nameV,
|
|
1890
|
+
redirect_uris: uris,
|
|
1891
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
1892
|
+
token_endpoint_auth_method: type === 'public' || type === 'native' || type === 'user-agent-based' ? 'none' : 'client_secret_basic',
|
|
1893
|
+
first_party: bool((0, upgrade_1.propOf)(c, 'skipConsent')) === true,
|
|
1894
|
+
});
|
|
1895
|
+
b.add('differs', area, `${name}.trustedClients "${nameV}"`, 'Created as an OAuth client with a NEW client id and secret (shown once on creation, or rotate with atlas client rotate). skipConsent → first_party (no consent screen).', c, 'oauth_clients');
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
else if (trusted)
|
|
1899
|
+
b.add('manual', area, `${name}.trustedClients`, `Cannot read the clients (${describe(trusted)}); create them as OAuth clients.`, trusted);
|
|
1900
|
+
b.add('manual', area, `${name} clients`, 'Clients registered at runtime live in your database (oauthApplication / oauthClient). Re-create each as an Atlas OAuth client and send the relying party its new client id and secret.', call, 'oauth_clients');
|
|
1901
|
+
for (const k of ['loginPage', 'consentPage']) {
|
|
1902
|
+
if ((0, upgrade_1.propOf)(o, k))
|
|
1903
|
+
b.add('mapped', area, `${name}.${k}`, 'Atlas hosts the sign-in and consent pages (branded); a custom consent UI can use the consent API.', (0, upgrade_1.propOf)(o, k));
|
|
1904
|
+
}
|
|
1905
|
+
for (const k of ['getAdditionalUserInfoClaim', 'customIdTokenClaims', 'customAccessTokenClaims', 'customUserInfoClaims']) {
|
|
1906
|
+
if ((0, upgrade_1.propOf)(o, k))
|
|
1907
|
+
b.add('manual', area, `${name}.${k}`, 'Custom claims become JWT templates released by scope (oauthProvider.tokenClaims).', (0, upgrade_1.propOf)(o, k), 'auth_config.oauthProvider.tokenClaims');
|
|
1908
|
+
}
|
|
1909
|
+
if ((0, upgrade_1.propOf)(o, 'useJWTPlugin'))
|
|
1910
|
+
b.add('mapped', area, `${name}.useJWTPlugin`, 'Access tokens are signed JWTs (accessTokenFormat "jwt").', (0, upgrade_1.propOf)(o, 'useJWTPlugin'));
|
|
1911
|
+
}
|
|
1912
|
+
// ───────────────────────────── payments ─────────────────────────────
|
|
1913
|
+
function translatePayments(b, p) {
|
|
1914
|
+
const provider = p.id.split(':')[1] ?? 'stripe';
|
|
1915
|
+
const area = `plugin:${provider}`;
|
|
1916
|
+
const o = p.options;
|
|
1917
|
+
b.add('mapped', area, `${p.call.callee}()`, `End-user billing on ${provider === 'stripe' ? 'Stripe' : provider} with your own account: connect it in Atlas (billing provider settings) with your API key and webhook secret; plans become Atlas billing plans and the plan slug is in the session token (pla). Existing subscriptions are imported with their customer and subscription ids.`, p.call, 'billing_plans');
|
|
1918
|
+
// Polar & co. configure products on a `checkout({ products })` entry of `use: [...]`.
|
|
1919
|
+
const use = (0, upgrade_1.propOf)(o, 'use');
|
|
1920
|
+
const checkoutCall = use?.kind === 'array' ? use.items.find((i) => i.kind === 'call' && /(^|\.)checkout$/i.test(i.callee)) : undefined;
|
|
1921
|
+
const checkoutProducts = checkoutCall?.kind === 'call' ? (0, upgrade_1.propOf)(checkoutCall.args[0], 'products') : undefined;
|
|
1922
|
+
const plans = provider === 'stripe'
|
|
1923
|
+
? (0, upgrade_1.propOf)((0, upgrade_1.propOf)(o, 'subscription'), 'plans')
|
|
1924
|
+
: ((0, upgrade_1.propOf)(o, 'products') ?? checkoutProducts ?? (0, upgrade_1.propOf)((0, upgrade_1.propOf)(o, 'checkout'), 'products') ?? (0, upgrade_1.propOf)(o, 'plans'));
|
|
1925
|
+
const orgBilling = bool((0, upgrade_1.propOf)((0, upgrade_1.propOf)(o, 'organization'), 'enabled')) === true;
|
|
1926
|
+
if (!plans) {
|
|
1927
|
+
if (provider !== 'stripe')
|
|
1928
|
+
b.add('manual', area, `${p.call.callee}() products`, 'Create the billing plans in Atlas with their product / price ids (billing_plans).', p.call, 'billing_plans');
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
if (plans.kind === 'function') {
|
|
1932
|
+
b.add('manual', area, 'subscription.plans', 'Plans come from a function at runtime; create the billing plans in Atlas (billing_plans) with their price ids.', plans, 'billing_plans');
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
if (plans.kind !== 'array') {
|
|
1936
|
+
b.add('manual', area, 'subscription.plans', `Cannot read the plans (${describe(plans)}).`, plans, 'billing_plans');
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
for (const plan of plans.items) {
|
|
1940
|
+
const name = str((0, upgrade_1.propOf)(plan, 'name')) ?? str((0, upgrade_1.propOf)(plan, 'slug')) ?? str((0, upgrade_1.propOf)(plan, 'productId'));
|
|
1941
|
+
if (!name) {
|
|
1942
|
+
b.add('manual', area, 'plan', `Cannot read this plan (${describe(plan)}).`, plan, 'billing_plans');
|
|
1943
|
+
continue;
|
|
1944
|
+
}
|
|
1945
|
+
const planSlug = slug(str((0, upgrade_1.propOf)(plan, 'slug')) ?? name, 64);
|
|
1946
|
+
const priceV = (0, upgrade_1.propOf)(plan, 'priceId') ?? (0, upgrade_1.propOf)(plan, 'productId') ?? (0, upgrade_1.propOf)(plan, 'id');
|
|
1947
|
+
const doc = { name: titleCase(name), slug: planSlug, interval: 'month', active: true, audience: orgBilling ? 'org' : 'user' };
|
|
1948
|
+
if (provider !== 'stripe')
|
|
1949
|
+
doc.provider = provider;
|
|
1950
|
+
if (priceV) {
|
|
1951
|
+
const price = b.configString(priceV, `${name} price id`, `${slug(name).toUpperCase()}_PRICE_ID`, { area, option: `plan "${name}" priceId` });
|
|
1952
|
+
if (provider === 'stripe')
|
|
1953
|
+
doc.stripe_price_id = price;
|
|
1954
|
+
else
|
|
1955
|
+
doc.provider_price_id = price;
|
|
1956
|
+
}
|
|
1957
|
+
else if ((0, upgrade_1.propOf)(plan, 'lookupKey')) {
|
|
1958
|
+
b.add('manual', area, `plan "${name}" lookupKey`, 'Atlas references Stripe prices by id: replace the lookup key with the price id.', (0, upgrade_1.propOf)(plan, 'lookupKey'), 'billing_plans stripe_price_id');
|
|
1959
|
+
}
|
|
1960
|
+
const annual = (0, upgrade_1.propOf)(plan, 'annualDiscountPriceId');
|
|
1961
|
+
if (annual)
|
|
1962
|
+
doc.annual_price_id = b.configString(annual, `${name} annual price id`, `${slug(name).toUpperCase()}_ANNUAL_PRICE_ID`, { area, option: `plan "${name}" annualDiscountPriceId` });
|
|
1963
|
+
const limits = (0, upgrade_1.literalOf)((0, upgrade_1.propOf)(plan, 'limits'));
|
|
1964
|
+
if (limits && typeof limits === 'object' && !Array.isArray(limits))
|
|
1965
|
+
doc.limits = limits;
|
|
1966
|
+
else if ((0, upgrade_1.propOf)(plan, 'limits'))
|
|
1967
|
+
b.add('manual', area, `plan "${name}" limits`, 'Cannot read the limits; set them on the plan.', (0, upgrade_1.propOf)(plan, 'limits'), 'billing_plans limits');
|
|
1968
|
+
const trial = num((0, upgrade_1.propOf)((0, upgrade_1.propOf)(plan, 'freeTrial'), 'days'));
|
|
1969
|
+
if (trial !== undefined)
|
|
1970
|
+
doc.trial_days = clamp(trial, 0, 3650);
|
|
1971
|
+
if ((0, upgrade_1.propOf)(plan, 'seatPriceId')) {
|
|
1972
|
+
doc.pricing_model = 'per_seat';
|
|
1973
|
+
b.add('differs', area, `plan "${name}" seatPriceId`, 'Per-seat pricing: the plan price is charged per member.', (0, upgrade_1.propOf)(plan, 'seatPriceId'), 'billing_plans pricing_model');
|
|
1974
|
+
}
|
|
1975
|
+
if ((0, upgrade_1.propOf)(plan, 'group'))
|
|
1976
|
+
b.add('differs', area, `plan "${name}" group`, 'Plan groups are expressed with kind base/addon.', (0, upgrade_1.propOf)(plan, 'group'));
|
|
1977
|
+
b.doc.billing_plans.push(doc);
|
|
1978
|
+
b.add('mapped', area, `plan "${name}"`, `Billing plan "${planSlug}"${doc.trial_days !== undefined ? ` with a ${doc.trial_days}-day trial` : ''}. Set the amount and currency for display.`, plan, `billing_plans ${planSlug}`);
|
|
1979
|
+
}
|
|
1980
|
+
if (bool((0, upgrade_1.propOf)(o, 'createCustomerOnSignUp')))
|
|
1981
|
+
b.add('mapped', area, 'createCustomerOnSignUp', 'A billing customer is created when it is first needed (checkout or portal).', (0, upgrade_1.propOf)(o, 'createCustomerOnSignUp'));
|
|
1982
|
+
for (const k of ['onEvent', 'onSubscriptionComplete', 'onSubscriptionUpdate', 'onSubscriptionCancel', 'onCustomerCreate']) {
|
|
1983
|
+
if ((0, upgrade_1.propOf)(o, k) || (0, upgrade_1.propOf)((0, upgrade_1.propOf)(o, 'subscription'), k))
|
|
1984
|
+
b.add('manual', area, k, 'Move this to a webhook on billing.subscription.* events.', (0, upgrade_1.propOf)(o, k) ?? (0, upgrade_1.propOf)((0, upgrade_1.propOf)(o, 'subscription'), k));
|
|
1985
|
+
}
|
|
1986
|
+
if ((0, upgrade_1.propOf)(o, 'stripeWebhookSecret') || (0, upgrade_1.propOf)(o, 'webhookSecret'))
|
|
1987
|
+
b.add('manual', area, 'webhook secret', 'Point your billing provider\'s webhook at Atlas (the billing provider settings show the URL) and store the new signing secret there.', (0, upgrade_1.propOf)(o, 'stripeWebhookSecret') ?? (0, upgrade_1.propOf)(o, 'webhookSecret'));
|
|
1988
|
+
}
|
|
1989
|
+
//# sourceMappingURL=translate.js.map
|