@objectstack/plugin-auth 4.0.4 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/index.d.mts +441 -19940
- package/dist/index.d.ts +441 -19940
- package/dist/index.js +704 -900
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +699 -880
- package/dist/index.mjs.map +1 -1
- package/package.json +35 -12
- package/.turbo/turbo-build.log +0 -78
- package/ARCHITECTURE.md +0 -176
- package/CHANGELOG.md +0 -333
- package/IMPLEMENTATION_SUMMARY.md +0 -192
- package/examples/basic-usage.ts +0 -107
- package/objectstack.config.ts +0 -24
- package/src/auth-manager.test.ts +0 -883
- package/src/auth-manager.ts +0 -419
- package/src/auth-plugin.test.ts +0 -446
- package/src/auth-plugin.ts +0 -314
- package/src/auth-schema-config.ts +0 -339
- package/src/index.ts +0 -16
- package/src/objectql-adapter.test.ts +0 -281
- package/src/objectql-adapter.ts +0 -279
- package/src/objects/auth-account.object.ts +0 -7
- package/src/objects/auth-session.object.ts +0 -7
- package/src/objects/auth-user.object.ts +0 -7
- package/src/objects/auth-verification.object.ts +0 -7
- package/src/objects/index.ts +0 -40
- package/src/objects/sys-account.object.ts +0 -111
- package/src/objects/sys-api-key.object.ts +0 -104
- package/src/objects/sys-invitation.object.ts +0 -93
- package/src/objects/sys-member.object.ts +0 -68
- package/src/objects/sys-organization.object.ts +0 -82
- package/src/objects/sys-session.object.ts +0 -84
- package/src/objects/sys-team-member.object.ts +0 -61
- package/src/objects/sys-team.object.ts +0 -69
- package/src/objects/sys-two-factor.object.ts +0 -73
- package/src/objects/sys-user-preference.object.ts +0 -82
- package/src/objects/sys-user.object.ts +0 -91
- package/src/objects/sys-verification.object.ts +0 -75
- package/tsconfig.json +0 -18
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,15 +17,32 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
21
31
|
var index_exports = {};
|
|
22
32
|
__export(index_exports, {
|
|
23
33
|
AUTH_ACCOUNT_CONFIG: () => AUTH_ACCOUNT_CONFIG,
|
|
34
|
+
AUTH_ADMIN_SESSION_FIELDS: () => AUTH_ADMIN_SESSION_FIELDS,
|
|
35
|
+
AUTH_ADMIN_USER_FIELDS: () => AUTH_ADMIN_USER_FIELDS,
|
|
36
|
+
AUTH_DEVICE_CODE_SCHEMA: () => AUTH_DEVICE_CODE_SCHEMA,
|
|
24
37
|
AUTH_INVITATION_SCHEMA: () => AUTH_INVITATION_SCHEMA,
|
|
38
|
+
AUTH_JWKS_SCHEMA: () => AUTH_JWKS_SCHEMA,
|
|
25
39
|
AUTH_MEMBER_SCHEMA: () => AUTH_MEMBER_SCHEMA,
|
|
26
40
|
AUTH_MODEL_TO_PROTOCOL: () => AUTH_MODEL_TO_PROTOCOL,
|
|
41
|
+
AUTH_OAUTH_ACCESS_TOKEN_SCHEMA: () => AUTH_OAUTH_ACCESS_TOKEN_SCHEMA,
|
|
42
|
+
AUTH_OAUTH_APPLICATION_SCHEMA: () => AUTH_OAUTH_APPLICATION_SCHEMA,
|
|
43
|
+
AUTH_OAUTH_CLIENT_SCHEMA: () => AUTH_OAUTH_CLIENT_SCHEMA,
|
|
44
|
+
AUTH_OAUTH_CONSENT_SCHEMA: () => AUTH_OAUTH_CONSENT_SCHEMA,
|
|
45
|
+
AUTH_OAUTH_REFRESH_TOKEN_SCHEMA: () => AUTH_OAUTH_REFRESH_TOKEN_SCHEMA,
|
|
27
46
|
AUTH_ORGANIZATION_SCHEMA: () => AUTH_ORGANIZATION_SCHEMA,
|
|
28
47
|
AUTH_ORG_SESSION_FIELDS: () => AUTH_ORG_SESSION_FIELDS,
|
|
29
48
|
AUTH_SESSION_CONFIG: () => AUTH_SESSION_CONFIG,
|
|
@@ -33,24 +52,13 @@ __export(index_exports, {
|
|
|
33
52
|
AUTH_TWO_FACTOR_USER_FIELDS: () => AUTH_TWO_FACTOR_USER_FIELDS,
|
|
34
53
|
AUTH_USER_CONFIG: () => AUTH_USER_CONFIG,
|
|
35
54
|
AUTH_VERIFICATION_CONFIG: () => AUTH_VERIFICATION_CONFIG,
|
|
36
|
-
AuthAccount: () => SysAccount,
|
|
37
55
|
AuthManager: () => AuthManager,
|
|
38
56
|
AuthPlugin: () => AuthPlugin,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
SysInvitation: () => SysInvitation,
|
|
45
|
-
SysMember: () => SysMember,
|
|
46
|
-
SysOrganization: () => SysOrganization,
|
|
47
|
-
SysSession: () => SysSession,
|
|
48
|
-
SysTeam: () => SysTeam,
|
|
49
|
-
SysTeamMember: () => SysTeamMember,
|
|
50
|
-
SysTwoFactor: () => SysTwoFactor,
|
|
51
|
-
SysUser: () => SysUser,
|
|
52
|
-
SysUserPreference: () => SysUserPreference,
|
|
53
|
-
SysVerification: () => SysVerification,
|
|
57
|
+
buildAdminPluginSchema: () => buildAdminPluginSchema,
|
|
58
|
+
buildDeviceAuthorizationPluginSchema: () => buildDeviceAuthorizationPluginSchema,
|
|
59
|
+
buildJwtPluginSchema: () => buildJwtPluginSchema,
|
|
60
|
+
buildOauthProviderPluginSchema: () => buildOauthProviderPluginSchema,
|
|
61
|
+
buildOidcProviderPluginSchema: () => buildOidcProviderPluginSchema,
|
|
54
62
|
buildOrganizationPluginSchema: () => buildOrganizationPluginSchema,
|
|
55
63
|
buildTwoFactorPluginSchema: () => buildTwoFactorPluginSchema,
|
|
56
64
|
createObjectQLAdapter: () => createObjectQLAdapter,
|
|
@@ -59,11 +67,8 @@ __export(index_exports, {
|
|
|
59
67
|
});
|
|
60
68
|
module.exports = __toCommonJS(index_exports);
|
|
61
69
|
|
|
62
|
-
// src/auth-
|
|
63
|
-
var
|
|
64
|
-
var import_organization = require("better-auth/plugins/organization");
|
|
65
|
-
var import_two_factor = require("better-auth/plugins/two-factor");
|
|
66
|
-
var import_magic_link = require("better-auth/plugins/magic-link");
|
|
70
|
+
// src/auth-plugin.ts
|
|
71
|
+
var import_apps = require("@objectstack/platform-objects/apps");
|
|
67
72
|
|
|
68
73
|
// src/objectql-adapter.ts
|
|
69
74
|
var import_adapters = require("better-auth/adapters");
|
|
@@ -77,6 +82,41 @@ var AUTH_MODEL_TO_PROTOCOL = {
|
|
|
77
82
|
function resolveProtocolName(model) {
|
|
78
83
|
return AUTH_MODEL_TO_PROTOCOL[model] ?? model;
|
|
79
84
|
}
|
|
85
|
+
var LEGACY_DATETIME_FIELDS_BY_MODEL = {
|
|
86
|
+
user: ["created_at", "updated_at"],
|
|
87
|
+
session: ["expires_at", "created_at", "updated_at"],
|
|
88
|
+
account: [
|
|
89
|
+
"access_token_expires_at",
|
|
90
|
+
"refresh_token_expires_at",
|
|
91
|
+
"created_at",
|
|
92
|
+
"updated_at"
|
|
93
|
+
],
|
|
94
|
+
verification: ["expires_at", "created_at", "updated_at"]
|
|
95
|
+
};
|
|
96
|
+
var NUMERIC_STRING_RE = /^-?\d+(\.\d+)?$/;
|
|
97
|
+
function normaliseLegacyDate(value) {
|
|
98
|
+
if (typeof value !== "string") return value;
|
|
99
|
+
if (!NUMERIC_STRING_RE.test(value)) return value;
|
|
100
|
+
const n = parseFloat(value);
|
|
101
|
+
if (!Number.isFinite(n)) return value;
|
|
102
|
+
if (Math.abs(n) < 1e10) return value;
|
|
103
|
+
const d = new Date(n);
|
|
104
|
+
if (Number.isNaN(d.getTime())) return value;
|
|
105
|
+
return d.toISOString();
|
|
106
|
+
}
|
|
107
|
+
function normaliseLegacyDates(model, record) {
|
|
108
|
+
if (!record) return record;
|
|
109
|
+
const cols = LEGACY_DATETIME_FIELDS_BY_MODEL[model];
|
|
110
|
+
if (!cols) return record;
|
|
111
|
+
for (const col of cols) {
|
|
112
|
+
if (col in record) {
|
|
113
|
+
record[col] = normaliseLegacyDate(
|
|
114
|
+
record[col]
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return record;
|
|
119
|
+
}
|
|
80
120
|
function convertWhere(where) {
|
|
81
121
|
const filter = {};
|
|
82
122
|
for (const condition of where) {
|
|
@@ -105,20 +145,24 @@ function createObjectQLAdapterFactory(dataEngine) {
|
|
|
105
145
|
return (0, import_adapters.createAdapterFactory)({
|
|
106
146
|
config: {
|
|
107
147
|
adapterId: "objectql",
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
148
|
+
// We let better-auth handle Date↔string and boolean↔0/1 conversion so
|
|
149
|
+
// that values land in the underlying SQL driver as primitive strings
|
|
150
|
+
// and integers. Some drivers (e.g. libsql over the HTTP transport)
|
|
151
|
+
// otherwise mangle `Date` objects into `"<epoch>.0"` strings that
|
|
152
|
+
// break the client-side session parser.
|
|
153
|
+
supportsBooleans: false,
|
|
154
|
+
supportsDates: false,
|
|
111
155
|
supportsJSON: true
|
|
112
156
|
},
|
|
113
157
|
adapter: () => ({
|
|
114
158
|
create: async ({ model, data, select: _select }) => {
|
|
115
159
|
const result = await dataEngine.insert(model, data);
|
|
116
|
-
return result;
|
|
160
|
+
return normaliseLegacyDates(model, result);
|
|
117
161
|
},
|
|
118
162
|
findOne: async ({ model, where, select, join: _join }) => {
|
|
119
163
|
const filter = convertWhere(where);
|
|
120
164
|
const result = await dataEngine.findOne(model, { where: filter, fields: select });
|
|
121
|
-
return result ? result : null;
|
|
165
|
+
return result ? normaliseLegacyDates(model, result) : null;
|
|
122
166
|
},
|
|
123
167
|
findMany: async ({ model, where, limit, offset, sortBy, join: _join }) => {
|
|
124
168
|
const filter = where ? convertWhere(where) : {};
|
|
@@ -129,7 +173,7 @@ function createObjectQLAdapterFactory(dataEngine) {
|
|
|
129
173
|
offset,
|
|
130
174
|
orderBy
|
|
131
175
|
});
|
|
132
|
-
return results;
|
|
176
|
+
return results.map((r) => normaliseLegacyDates(model, r));
|
|
133
177
|
},
|
|
134
178
|
count: async ({ model, where }) => {
|
|
135
179
|
const filter = where ? convertWhere(where) : {};
|
|
@@ -140,7 +184,7 @@ function createObjectQLAdapterFactory(dataEngine) {
|
|
|
140
184
|
const record = await dataEngine.findOne(model, { where: filter });
|
|
141
185
|
if (!record) return null;
|
|
142
186
|
const result = await dataEngine.update(model, { ...update, id: record.id });
|
|
143
|
-
return result ? result : null;
|
|
187
|
+
return result ? normaliseLegacyDates(model, result) : null;
|
|
144
188
|
},
|
|
145
189
|
updateMany: async ({ model, where, update }) => {
|
|
146
190
|
const filter = convertWhere(where);
|
|
@@ -337,6 +381,88 @@ var AUTH_TWO_FACTOR_SCHEMA = {
|
|
|
337
381
|
var AUTH_TWO_FACTOR_USER_FIELDS = {
|
|
338
382
|
twoFactorEnabled: "two_factor_enabled"
|
|
339
383
|
};
|
|
384
|
+
var AUTH_ADMIN_USER_FIELDS = {
|
|
385
|
+
banReason: "ban_reason",
|
|
386
|
+
banExpires: "ban_expires"
|
|
387
|
+
};
|
|
388
|
+
var AUTH_ADMIN_SESSION_FIELDS = {
|
|
389
|
+
impersonatedBy: "impersonated_by"
|
|
390
|
+
};
|
|
391
|
+
var AUTH_OAUTH_CLIENT_SCHEMA = {
|
|
392
|
+
modelName: import_system2.SystemObjectName.OAUTH_APPLICATION,
|
|
393
|
+
// 'sys_oauth_application'
|
|
394
|
+
fields: {
|
|
395
|
+
clientId: "client_id",
|
|
396
|
+
clientSecret: "client_secret",
|
|
397
|
+
skipConsent: "skip_consent",
|
|
398
|
+
enableEndSession: "enable_end_session",
|
|
399
|
+
subjectType: "subject_type",
|
|
400
|
+
userId: "user_id",
|
|
401
|
+
createdAt: "created_at",
|
|
402
|
+
updatedAt: "updated_at",
|
|
403
|
+
redirectUris: "redirect_uris",
|
|
404
|
+
postLogoutRedirectUris: "post_logout_redirect_uris",
|
|
405
|
+
tokenEndpointAuthMethod: "token_endpoint_auth_method",
|
|
406
|
+
grantTypes: "grant_types",
|
|
407
|
+
responseTypes: "response_types",
|
|
408
|
+
requirePKCE: "require_pkce",
|
|
409
|
+
softwareId: "software_id",
|
|
410
|
+
softwareVersion: "software_version",
|
|
411
|
+
softwareStatement: "software_statement",
|
|
412
|
+
referenceId: "reference_id"
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
var AUTH_OAUTH_APPLICATION_SCHEMA = AUTH_OAUTH_CLIENT_SCHEMA;
|
|
416
|
+
var AUTH_OAUTH_ACCESS_TOKEN_SCHEMA = {
|
|
417
|
+
modelName: import_system2.SystemObjectName.OAUTH_ACCESS_TOKEN,
|
|
418
|
+
// 'sys_oauth_access_token'
|
|
419
|
+
fields: {
|
|
420
|
+
clientId: "client_id",
|
|
421
|
+
sessionId: "session_id",
|
|
422
|
+
userId: "user_id",
|
|
423
|
+
referenceId: "reference_id",
|
|
424
|
+
refreshId: "refresh_id",
|
|
425
|
+
expiresAt: "expires_at",
|
|
426
|
+
createdAt: "created_at"
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
var AUTH_OAUTH_REFRESH_TOKEN_SCHEMA = {
|
|
430
|
+
modelName: import_system2.SystemObjectName.OAUTH_REFRESH_TOKEN,
|
|
431
|
+
// 'sys_oauth_refresh_token'
|
|
432
|
+
fields: {
|
|
433
|
+
clientId: "client_id",
|
|
434
|
+
sessionId: "session_id",
|
|
435
|
+
userId: "user_id",
|
|
436
|
+
referenceId: "reference_id",
|
|
437
|
+
expiresAt: "expires_at",
|
|
438
|
+
createdAt: "created_at",
|
|
439
|
+
authTime: "auth_time"
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
var AUTH_OAUTH_CONSENT_SCHEMA = {
|
|
443
|
+
modelName: import_system2.SystemObjectName.OAUTH_CONSENT,
|
|
444
|
+
// 'sys_oauth_consent'
|
|
445
|
+
fields: {
|
|
446
|
+
clientId: "client_id",
|
|
447
|
+
userId: "user_id",
|
|
448
|
+
referenceId: "reference_id",
|
|
449
|
+
createdAt: "created_at",
|
|
450
|
+
updatedAt: "updated_at"
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
var AUTH_DEVICE_CODE_SCHEMA = {
|
|
454
|
+
modelName: import_system2.SystemObjectName.DEVICE_CODE,
|
|
455
|
+
// 'sys_device_code'
|
|
456
|
+
fields: {
|
|
457
|
+
deviceCode: "device_code",
|
|
458
|
+
userCode: "user_code",
|
|
459
|
+
userId: "user_id",
|
|
460
|
+
expiresAt: "expires_at",
|
|
461
|
+
lastPolledAt: "last_polled_at",
|
|
462
|
+
pollingInterval: "polling_interval",
|
|
463
|
+
clientId: "client_id"
|
|
464
|
+
}
|
|
465
|
+
};
|
|
340
466
|
function buildTwoFactorPluginSchema() {
|
|
341
467
|
return {
|
|
342
468
|
twoFactor: AUTH_TWO_FACTOR_SCHEMA,
|
|
@@ -345,6 +471,16 @@ function buildTwoFactorPluginSchema() {
|
|
|
345
471
|
}
|
|
346
472
|
};
|
|
347
473
|
}
|
|
474
|
+
function buildAdminPluginSchema() {
|
|
475
|
+
return {
|
|
476
|
+
user: {
|
|
477
|
+
fields: AUTH_ADMIN_USER_FIELDS
|
|
478
|
+
},
|
|
479
|
+
session: {
|
|
480
|
+
fields: AUTH_ADMIN_SESSION_FIELDS
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
}
|
|
348
484
|
function buildOrganizationPluginSchema() {
|
|
349
485
|
return {
|
|
350
486
|
organization: AUTH_ORGANIZATION_SCHEMA,
|
|
@@ -357,6 +493,35 @@ function buildOrganizationPluginSchema() {
|
|
|
357
493
|
}
|
|
358
494
|
};
|
|
359
495
|
}
|
|
496
|
+
var AUTH_JWKS_SCHEMA = {
|
|
497
|
+
modelName: import_system2.SystemObjectName.JWKS,
|
|
498
|
+
// 'sys_jwks'
|
|
499
|
+
fields: {
|
|
500
|
+
publicKey: "public_key",
|
|
501
|
+
privateKey: "private_key",
|
|
502
|
+
createdAt: "created_at",
|
|
503
|
+
expiresAt: "expires_at"
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
function buildJwtPluginSchema() {
|
|
507
|
+
return {
|
|
508
|
+
jwks: AUTH_JWKS_SCHEMA
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function buildOauthProviderPluginSchema() {
|
|
512
|
+
return {
|
|
513
|
+
oauthClient: AUTH_OAUTH_CLIENT_SCHEMA,
|
|
514
|
+
oauthAccessToken: AUTH_OAUTH_ACCESS_TOKEN_SCHEMA,
|
|
515
|
+
oauthRefreshToken: AUTH_OAUTH_REFRESH_TOKEN_SCHEMA,
|
|
516
|
+
oauthConsent: AUTH_OAUTH_CONSENT_SCHEMA
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
var buildOidcProviderPluginSchema = buildOauthProviderPluginSchema;
|
|
520
|
+
function buildDeviceAuthorizationPluginSchema() {
|
|
521
|
+
return {
|
|
522
|
+
deviceCode: AUTH_DEVICE_CODE_SCHEMA
|
|
523
|
+
};
|
|
524
|
+
}
|
|
360
525
|
|
|
361
526
|
// src/auth-manager.ts
|
|
362
527
|
var AuthManager = class {
|
|
@@ -370,16 +535,18 @@ var AuthManager = class {
|
|
|
370
535
|
/**
|
|
371
536
|
* Get or create the better-auth instance (lazy initialization)
|
|
372
537
|
*/
|
|
373
|
-
getOrCreateAuth() {
|
|
538
|
+
async getOrCreateAuth() {
|
|
374
539
|
if (!this.auth) {
|
|
375
|
-
this.auth = this.createAuthInstance();
|
|
540
|
+
this.auth = await this.createAuthInstance();
|
|
376
541
|
}
|
|
377
542
|
return this.auth;
|
|
378
543
|
}
|
|
379
544
|
/**
|
|
380
545
|
* Create a better-auth instance from configuration
|
|
381
546
|
*/
|
|
382
|
-
createAuthInstance() {
|
|
547
|
+
async createAuthInstance() {
|
|
548
|
+
const { betterAuth } = await import("better-auth");
|
|
549
|
+
const plugins = await this.buildPluginList();
|
|
383
550
|
const betterAuthConfig = {
|
|
384
551
|
// Base configuration
|
|
385
552
|
secret: this.config.secret || this.generateSecret(),
|
|
@@ -395,7 +562,41 @@ var AuthManager = class {
|
|
|
395
562
|
...AUTH_USER_CONFIG
|
|
396
563
|
},
|
|
397
564
|
account: {
|
|
398
|
-
...AUTH_ACCOUNT_CONFIG
|
|
565
|
+
...AUTH_ACCOUNT_CONFIG,
|
|
566
|
+
// Allow OIDC/OAuth callbacks to implicitly link the incoming
|
|
567
|
+
// identity to a pre-existing local user when the emails match.
|
|
568
|
+
//
|
|
569
|
+
// ObjectStack's platform SSO ("objectstack-cloud" provider) is the
|
|
570
|
+
// canonical case: cloud is the IdP for every project, so a user
|
|
571
|
+
// arriving via SSO is — by construction — the same person who was
|
|
572
|
+
// auto-seeded as the project owner when the project was created.
|
|
573
|
+
// Without trusting the provider, better-auth's safety check rejects
|
|
574
|
+
// the link with `error=account_not_linked` because the seeded user
|
|
575
|
+
// row has `emailVerified=false` (no actual verification ever runs
|
|
576
|
+
// in the IdP-mediated flow). See packages/plugins/plugin-auth/
|
|
577
|
+
// node_modules/better-auth/dist/oauth2/link-account.mjs:22.
|
|
578
|
+
//
|
|
579
|
+
// Custom-deployment consumers can extend the trusted set via
|
|
580
|
+
// `config.account.accountLinking.trustedProviders`; we always
|
|
581
|
+
// include `objectstack-cloud` because it is the platform IdP.
|
|
582
|
+
accountLinking: {
|
|
583
|
+
enabled: true,
|
|
584
|
+
// better-auth's account-linking gate has TWO independent clauses
|
|
585
|
+
// (see link-account.mjs:22). Trusting the provider only satisfies
|
|
586
|
+
// the first clause; the second — `requireLocalEmailVerified &&
|
|
587
|
+
// !dbUser.user.emailVerified` — still blocks linking when the
|
|
588
|
+
// pre-existing local user row has `emailVerified=false` (the
|
|
589
|
+
// default for owner-seeded rows). Disabling the local-email gate
|
|
590
|
+
// is safe here because the OAuth side is what we actually trust:
|
|
591
|
+
// the incoming identity was verified by the IdP. Consumers who
|
|
592
|
+
// need the stricter behavior can override via config.
|
|
593
|
+
requireLocalEmailVerified: false,
|
|
594
|
+
...this.config?.account?.accountLinking ?? {},
|
|
595
|
+
trustedProviders: Array.from(/* @__PURE__ */ new Set([
|
|
596
|
+
"objectstack-cloud",
|
|
597
|
+
...this.config?.account?.accountLinking?.trustedProviders ?? []
|
|
598
|
+
]))
|
|
599
|
+
}
|
|
399
600
|
},
|
|
400
601
|
verification: {
|
|
401
602
|
...AUTH_VERIFICATION_CONFIG
|
|
@@ -411,15 +612,71 @@ var AuthManager = class {
|
|
|
411
612
|
...this.config.emailAndPassword?.maxPasswordLength != null ? { maxPasswordLength: this.config.emailAndPassword.maxPasswordLength } : {},
|
|
412
613
|
...this.config.emailAndPassword?.resetPasswordTokenExpiresIn != null ? { resetPasswordTokenExpiresIn: this.config.emailAndPassword.resetPasswordTokenExpiresIn } : {},
|
|
413
614
|
...this.config.emailAndPassword?.autoSignIn != null ? { autoSignIn: this.config.emailAndPassword.autoSignIn } : {},
|
|
414
|
-
...this.config.emailAndPassword?.revokeSessionsOnPasswordReset != null ? { revokeSessionsOnPasswordReset: this.config.emailAndPassword.revokeSessionsOnPasswordReset } : {}
|
|
615
|
+
...this.config.emailAndPassword?.revokeSessionsOnPasswordReset != null ? { revokeSessionsOnPasswordReset: this.config.emailAndPassword.revokeSessionsOnPasswordReset } : {},
|
|
616
|
+
sendResetPassword: async ({ user, url, token }) => {
|
|
617
|
+
const email = this.getEmailService();
|
|
618
|
+
if (!email) {
|
|
619
|
+
console.warn(
|
|
620
|
+
`[AuthManager] Password-reset requested for ${user.email} but no email service is wired. URL: ${url}`
|
|
621
|
+
);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const ttlSec = this.config.emailAndPassword?.resetPasswordTokenExpiresIn ?? 60 * 60;
|
|
625
|
+
try {
|
|
626
|
+
await email.sendTemplate({
|
|
627
|
+
template: "auth.password_reset",
|
|
628
|
+
to: { address: user.email, ...user.name ? { name: user.name } : {} },
|
|
629
|
+
data: {
|
|
630
|
+
user: { name: user.name || user.email, email: user.email, id: user.id },
|
|
631
|
+
resetUrl: url,
|
|
632
|
+
token,
|
|
633
|
+
expiresInMinutes: Math.round(ttlSec / 60),
|
|
634
|
+
appName: this.getAppName()
|
|
635
|
+
},
|
|
636
|
+
relatedObject: "sys_user",
|
|
637
|
+
relatedId: user.id
|
|
638
|
+
});
|
|
639
|
+
} catch (err) {
|
|
640
|
+
console.error(`[AuthManager] sendResetPassword failed: ${err?.message ?? err}`);
|
|
641
|
+
throw err;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
415
644
|
},
|
|
416
645
|
// Email verification
|
|
417
|
-
...this.config.emailVerification ? {
|
|
646
|
+
...this.config.emailVerification || this.config.emailService ? {
|
|
418
647
|
emailVerification: {
|
|
419
|
-
...this.config.emailVerification
|
|
420
|
-
...this.config.emailVerification
|
|
421
|
-
...this.config.emailVerification
|
|
422
|
-
...this.config.emailVerification
|
|
648
|
+
...this.config.emailVerification?.sendOnSignUp != null ? { sendOnSignUp: this.config.emailVerification.sendOnSignUp } : {},
|
|
649
|
+
...this.config.emailVerification?.sendOnSignIn != null ? { sendOnSignIn: this.config.emailVerification.sendOnSignIn } : {},
|
|
650
|
+
...this.config.emailVerification?.autoSignInAfterVerification != null ? { autoSignInAfterVerification: this.config.emailVerification.autoSignInAfterVerification } : {},
|
|
651
|
+
...this.config.emailVerification?.expiresIn != null ? { expiresIn: this.config.emailVerification.expiresIn } : {},
|
|
652
|
+
sendVerificationEmail: async ({ user, url, token }) => {
|
|
653
|
+
const email = this.getEmailService();
|
|
654
|
+
if (!email) {
|
|
655
|
+
console.warn(
|
|
656
|
+
`[AuthManager] Verification email requested for ${user.email} but no email service is wired. URL: ${url}`
|
|
657
|
+
);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const ttlSec = this.config.emailVerification?.expiresIn ?? 60 * 60;
|
|
661
|
+
try {
|
|
662
|
+
await email.sendTemplate({
|
|
663
|
+
template: "auth.verify_email",
|
|
664
|
+
to: { address: user.email, ...user.name ? { name: user.name } : {} },
|
|
665
|
+
data: {
|
|
666
|
+
user: { name: user.name || user.email, email: user.email, id: user.id },
|
|
667
|
+
verificationUrl: url,
|
|
668
|
+
token,
|
|
669
|
+
expiresInMinutes: Math.round(ttlSec / 60),
|
|
670
|
+
appName: this.getAppName()
|
|
671
|
+
},
|
|
672
|
+
relatedObject: "sys_user",
|
|
673
|
+
relatedId: user.id
|
|
674
|
+
});
|
|
675
|
+
} catch (err) {
|
|
676
|
+
console.error(`[AuthManager] sendVerificationEmail failed: ${err?.message ?? err}`);
|
|
677
|
+
throw err;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
423
680
|
}
|
|
424
681
|
} : {},
|
|
425
682
|
// Session configuration
|
|
@@ -431,7 +688,7 @@ var AuthManager = class {
|
|
|
431
688
|
// 1 day default
|
|
432
689
|
},
|
|
433
690
|
// better-auth plugins — registered based on AuthPluginConfig flags
|
|
434
|
-
plugins
|
|
691
|
+
plugins,
|
|
435
692
|
// Trusted origins for CSRF protection (supports wildcards like "https://*.example.com")
|
|
436
693
|
// Auto-includes origins from CORS_ORIGIN env var so CORS and CSRF stay in sync.
|
|
437
694
|
...(() => {
|
|
@@ -444,6 +701,8 @@ var AuthManager = class {
|
|
|
444
701
|
}
|
|
445
702
|
if (!origins.length && (!corsOrigin || corsOrigin === "*")) {
|
|
446
703
|
origins.push("http://localhost:*");
|
|
704
|
+
origins.push("http://*.localhost:*");
|
|
705
|
+
origins.push("https://*.localhost:*");
|
|
447
706
|
}
|
|
448
707
|
return origins.length ? { trustedOrigins: origins } : {};
|
|
449
708
|
})(),
|
|
@@ -457,7 +716,7 @@ var AuthManager = class {
|
|
|
457
716
|
}
|
|
458
717
|
} : {}
|
|
459
718
|
};
|
|
460
|
-
return
|
|
719
|
+
return betterAuth(betterAuthConfig);
|
|
461
720
|
}
|
|
462
721
|
/**
|
|
463
722
|
* Build the list of better-auth plugins based on AuthPluginConfig flags.
|
|
@@ -466,28 +725,224 @@ var AuthManager = class {
|
|
|
466
725
|
* a `schema` option containing the appropriate snake_case field mappings,
|
|
467
726
|
* so that `createAdapterFactory` transforms them automatically.
|
|
468
727
|
*/
|
|
469
|
-
buildPluginList() {
|
|
470
|
-
const pluginConfig = this.config.plugins;
|
|
728
|
+
async buildPluginList() {
|
|
729
|
+
const pluginConfig = this.config.plugins ?? {};
|
|
471
730
|
const plugins = [];
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
731
|
+
const enabled = {
|
|
732
|
+
organization: pluginConfig.organization ?? true,
|
|
733
|
+
twoFactor: pluginConfig.twoFactor ?? false,
|
|
734
|
+
passkeys: pluginConfig.passkeys ?? false,
|
|
735
|
+
magicLink: pluginConfig.magicLink ?? false,
|
|
736
|
+
oidcProvider: pluginConfig.oidcProvider ?? false,
|
|
737
|
+
deviceAuthorization: pluginConfig.deviceAuthorization ?? false,
|
|
738
|
+
admin: pluginConfig.admin ?? false
|
|
739
|
+
};
|
|
740
|
+
const { bearer } = await import("better-auth/plugins/bearer");
|
|
741
|
+
plugins.push(bearer());
|
|
742
|
+
if (enabled.organization) {
|
|
743
|
+
const { organization } = await import("better-auth/plugins/organization");
|
|
744
|
+
let customOrgRoles;
|
|
745
|
+
const extra = this.config.additionalOrgRoles;
|
|
746
|
+
if (extra && extra.length > 0) {
|
|
747
|
+
try {
|
|
748
|
+
const accessMod = await import("better-auth/plugins/organization/access");
|
|
749
|
+
const { defaultAc, memberAc, defaultRoles: importedDefaultRoles } = accessMod;
|
|
750
|
+
const defaultRoles = importedDefaultRoles || null;
|
|
751
|
+
if (defaultAc && memberAc && typeof memberAc.statements === "object") {
|
|
752
|
+
const built = defaultRoles ? { ...defaultRoles } : {};
|
|
753
|
+
const stmts = memberAc.statements;
|
|
754
|
+
for (const name of extra) {
|
|
755
|
+
if (!name) continue;
|
|
756
|
+
if (built[name]) continue;
|
|
757
|
+
built[name] = defaultAc.newRole(stmts);
|
|
758
|
+
}
|
|
759
|
+
customOrgRoles = built;
|
|
760
|
+
}
|
|
761
|
+
} catch {
|
|
762
|
+
customOrgRoles = void 0;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
plugins.push(organization({
|
|
766
|
+
schema: buildOrganizationPluginSchema(),
|
|
767
|
+
// Enable the team sub-feature so the framework's `sys_team` /
|
|
768
|
+
// `sys_team_member` tables (already declared in platform-objects)
|
|
769
|
+
// are actually wired up to better-auth's CRUD endpoints
|
|
770
|
+
// (`/organization/{create,update,remove,list}-team[s]` and
|
|
771
|
+
// `/organization/{add,remove,list}-team-member[s]`). The Account
|
|
772
|
+
// portal exposes a Teams page; without this flag those endpoints
|
|
773
|
+
// 404 and the section silently breaks.
|
|
774
|
+
teams: { enabled: true },
|
|
775
|
+
// Without a mailer wired in framework, requiring email verification
|
|
776
|
+
// before accepting invitations dead-ends every invite flow with
|
|
777
|
+
// FORBIDDEN EMAIL_VERIFICATION_REQUIRED…. Default-off here keeps
|
|
778
|
+
// the built-in /accept-invitation route usable for pilots; operators
|
|
779
|
+
// who wire a real mailer can re-enable downstream.
|
|
780
|
+
requireEmailVerificationOnInvitation: false,
|
|
781
|
+
...customOrgRoles ? { roles: customOrgRoles } : {},
|
|
782
|
+
// No mailer is wired in framework yet — log the accept URL so
|
|
783
|
+
// operators / UI can fall back to copy-paste flows. Replace this
|
|
784
|
+
// with a real mail integration when available.
|
|
785
|
+
sendInvitationEmail: async ({ email: recipientEmail, invitation, organization: org, inviter }) => {
|
|
786
|
+
const baseUrl = (this.config.baseUrl ?? "").replace(/\/$/, "");
|
|
787
|
+
const acceptUrl = `${baseUrl}/accept-invitation/${invitation.id}`;
|
|
788
|
+
const emailService = this.getEmailService();
|
|
789
|
+
if (!emailService) {
|
|
790
|
+
console.warn(
|
|
791
|
+
`[AuthManager] Invitation email not configured. To: ${recipientEmail} (org: ${org?.name ?? invitation.organizationId}, role: ${invitation.role}, inviter: ${inviter?.user?.email ?? "unknown"}) URL: ${acceptUrl}`
|
|
792
|
+
);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
try {
|
|
796
|
+
await emailService.sendTemplate({
|
|
797
|
+
template: "auth.invitation",
|
|
798
|
+
to: recipientEmail,
|
|
799
|
+
data: {
|
|
800
|
+
inviter: {
|
|
801
|
+
name: inviter?.user?.name ?? inviter?.user?.email ?? "A teammate",
|
|
802
|
+
email: inviter?.user?.email ?? ""
|
|
803
|
+
},
|
|
804
|
+
organization: { name: org?.name ?? invitation.organizationId },
|
|
805
|
+
role: invitation.role || "",
|
|
806
|
+
acceptUrl,
|
|
807
|
+
appName: this.getAppName()
|
|
808
|
+
},
|
|
809
|
+
relatedObject: "sys_invitation",
|
|
810
|
+
relatedId: invitation.id
|
|
811
|
+
});
|
|
812
|
+
} catch (err) {
|
|
813
|
+
console.error(`[AuthManager] sendInvitationEmail failed: ${err?.message ?? err}`);
|
|
814
|
+
throw err;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
475
817
|
}));
|
|
476
818
|
}
|
|
477
|
-
if (
|
|
478
|
-
|
|
819
|
+
if (enabled.twoFactor) {
|
|
820
|
+
const { twoFactor } = await import("better-auth/plugins/two-factor");
|
|
821
|
+
plugins.push(twoFactor({
|
|
479
822
|
schema: buildTwoFactorPluginSchema()
|
|
480
823
|
}));
|
|
481
824
|
}
|
|
482
|
-
if (
|
|
483
|
-
plugins
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
825
|
+
if (enabled.admin) {
|
|
826
|
+
const { admin } = await import("better-auth/plugins/admin");
|
|
827
|
+
plugins.push(admin({
|
|
828
|
+
schema: buildAdminPluginSchema()
|
|
829
|
+
}));
|
|
830
|
+
}
|
|
831
|
+
if (enabled.magicLink) {
|
|
832
|
+
const { magicLink } = await import("better-auth/plugins/magic-link");
|
|
833
|
+
plugins.push(magicLink({
|
|
834
|
+
sendMagicLink: async ({ email: recipientEmail, url, token }) => {
|
|
835
|
+
const emailService = this.getEmailService();
|
|
836
|
+
if (!emailService) {
|
|
837
|
+
console.warn(
|
|
838
|
+
`[AuthManager] Magic-link requested for ${recipientEmail} but no email service is wired. URL: ${url}`
|
|
839
|
+
);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
try {
|
|
843
|
+
await emailService.sendTemplate({
|
|
844
|
+
template: "auth.magic_link",
|
|
845
|
+
to: recipientEmail,
|
|
846
|
+
data: {
|
|
847
|
+
magicLinkUrl: url,
|
|
848
|
+
token,
|
|
849
|
+
expiresInMinutes: 10,
|
|
850
|
+
appName: this.getAppName()
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
} catch (err) {
|
|
854
|
+
console.error(`[AuthManager] sendMagicLink failed: ${err?.message ?? err}`);
|
|
855
|
+
throw err;
|
|
856
|
+
}
|
|
488
857
|
}
|
|
489
858
|
}));
|
|
490
859
|
}
|
|
860
|
+
if (this.config.oidcProviders?.length) {
|
|
861
|
+
const { genericOAuth } = await import("better-auth/plugins/generic-oauth");
|
|
862
|
+
plugins.push(genericOAuth({
|
|
863
|
+
config: this.config.oidcProviders.map((p) => ({
|
|
864
|
+
providerId: p.providerId,
|
|
865
|
+
...p.discoveryUrl ? { discoveryUrl: p.discoveryUrl } : {},
|
|
866
|
+
...p.issuer ? { issuer: p.issuer } : {},
|
|
867
|
+
...p.authorizationUrl ? { authorizationUrl: p.authorizationUrl } : {},
|
|
868
|
+
...p.tokenUrl ? { tokenUrl: p.tokenUrl } : {},
|
|
869
|
+
...p.userInfoUrl ? { userInfoUrl: p.userInfoUrl } : {},
|
|
870
|
+
clientId: p.clientId,
|
|
871
|
+
clientSecret: p.clientSecret,
|
|
872
|
+
...p.scopes ? { scopes: p.scopes } : {},
|
|
873
|
+
...p.pkce != null ? { pkce: p.pkce } : {}
|
|
874
|
+
}))
|
|
875
|
+
}));
|
|
876
|
+
}
|
|
877
|
+
if (enabled.oidcProvider) {
|
|
878
|
+
const { jwt } = await import("better-auth/plugins");
|
|
879
|
+
plugins.push(jwt({ schema: buildJwtPluginSchema() }));
|
|
880
|
+
const { oauthProvider } = await import("@better-auth/oauth-provider");
|
|
881
|
+
const baseUrl = (this.config.baseUrl ?? "").replace(/\/$/, "");
|
|
882
|
+
plugins.push(oauthProvider({
|
|
883
|
+
// Account SPA renders both pages — see apps/account.
|
|
884
|
+
loginPage: `${baseUrl}/_account/login`,
|
|
885
|
+
consentPage: `${baseUrl}/_account/oauth/consent`,
|
|
886
|
+
schema: buildOauthProviderPluginSchema()
|
|
887
|
+
}));
|
|
888
|
+
}
|
|
889
|
+
if (enabled.deviceAuthorization) {
|
|
890
|
+
const { deviceAuthorization } = await import("better-auth/plugins/device-authorization");
|
|
891
|
+
const baseUrl = (this.config.baseUrl ?? "").replace(/\/$/, "");
|
|
892
|
+
plugins.push(deviceAuthorization({
|
|
893
|
+
verificationUri: `${baseUrl}/_account/auth/device`,
|
|
894
|
+
schema: buildDeviceAuthorizationPluginSchema()
|
|
895
|
+
}));
|
|
896
|
+
}
|
|
897
|
+
const dataEngine = this.config.dataEngine;
|
|
898
|
+
if (dataEngine) {
|
|
899
|
+
const { customSession } = await import("better-auth/plugins/custom-session");
|
|
900
|
+
plugins.push(customSession(async ({ user, session }) => {
|
|
901
|
+
if (!user?.id) return { user, session };
|
|
902
|
+
const isPlatformAdmin = async () => {
|
|
903
|
+
try {
|
|
904
|
+
const links = await dataEngine.find("sys_user_permission_set", {
|
|
905
|
+
where: { user_id: user.id },
|
|
906
|
+
limit: 50
|
|
907
|
+
});
|
|
908
|
+
const platformLinks = (Array.isArray(links) ? links : []).filter(
|
|
909
|
+
(l) => !l.organization_id
|
|
910
|
+
);
|
|
911
|
+
if (platformLinks.length === 0) return false;
|
|
912
|
+
const sets = await dataEngine.find("sys_permission_set", { limit: 50 });
|
|
913
|
+
const adminSet = (Array.isArray(sets) ? sets : []).find(
|
|
914
|
+
(r) => r.name === "admin_full_access"
|
|
915
|
+
);
|
|
916
|
+
if (!adminSet) return false;
|
|
917
|
+
return platformLinks.some(
|
|
918
|
+
(l) => l.permission_set_id === adminSet.id
|
|
919
|
+
);
|
|
920
|
+
} catch {
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
const isActiveOrgAdmin = async () => {
|
|
925
|
+
try {
|
|
926
|
+
const orgId = session?.activeOrganizationId;
|
|
927
|
+
if (!orgId) return false;
|
|
928
|
+
const members = await dataEngine.find("sys_member", {
|
|
929
|
+
where: { user_id: user.id, organization_id: orgId },
|
|
930
|
+
limit: 5
|
|
931
|
+
});
|
|
932
|
+
return (Array.isArray(members) ? members : []).some((m) => {
|
|
933
|
+
const raw = typeof m?.role === "string" ? m.role : "";
|
|
934
|
+
const roles = raw.split(",").map((s) => s.trim().toLowerCase());
|
|
935
|
+
return roles.includes("owner") || roles.includes("admin");
|
|
936
|
+
});
|
|
937
|
+
} catch {
|
|
938
|
+
return false;
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
const promote = await isPlatformAdmin() || await isActiveOrgAdmin();
|
|
942
|
+
if (!promote) return { user, session };
|
|
943
|
+
return { user: { ...user, role: "admin" }, session };
|
|
944
|
+
}));
|
|
945
|
+
}
|
|
491
946
|
return plugins;
|
|
492
947
|
}
|
|
493
948
|
/**
|
|
@@ -544,6 +999,26 @@ var AuthManager = class {
|
|
|
544
999
|
}
|
|
545
1000
|
this.config = { ...this.config, baseUrl: url };
|
|
546
1001
|
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Inject (or replace) the outbound email service used by better-auth
|
|
1004
|
+
* callbacks. Safe to call after construction but BEFORE the first
|
|
1005
|
+
* request hits the auth handler — callbacks read this via
|
|
1006
|
+
* {@link getEmailService} when invoked.
|
|
1007
|
+
*
|
|
1008
|
+
* AuthPlugin calls this on `kernel:ready` once `ctx.getService('email')`
|
|
1009
|
+
* resolves. For tests / serverless, callers may invoke directly.
|
|
1010
|
+
*/
|
|
1011
|
+
setEmailService(email) {
|
|
1012
|
+
this.config.emailService = email;
|
|
1013
|
+
}
|
|
1014
|
+
/** @internal Used by callback closures. */
|
|
1015
|
+
getEmailService() {
|
|
1016
|
+
return this.config.emailService;
|
|
1017
|
+
}
|
|
1018
|
+
/** @internal `{{appName}}` placeholder value for built-in templates. */
|
|
1019
|
+
getAppName() {
|
|
1020
|
+
return this.config.appName ?? "ObjectStack";
|
|
1021
|
+
}
|
|
547
1022
|
/**
|
|
548
1023
|
* Get the underlying better-auth instance
|
|
549
1024
|
* Useful for advanced use cases
|
|
@@ -563,7 +1038,7 @@ var AuthManager = class {
|
|
|
563
1038
|
* @returns Web standard Response object
|
|
564
1039
|
*/
|
|
565
1040
|
async handleRequest(request) {
|
|
566
|
-
const auth = this.getOrCreateAuth();
|
|
1041
|
+
const auth = await this.getOrCreateAuth();
|
|
567
1042
|
const response = await auth.handler(request);
|
|
568
1043
|
if (response.status >= 500) {
|
|
569
1044
|
try {
|
|
@@ -579,18 +1054,19 @@ var AuthManager = class {
|
|
|
579
1054
|
* Get the better-auth API for programmatic access
|
|
580
1055
|
* Use this for server-side operations (e.g., creating users, checking sessions)
|
|
581
1056
|
*/
|
|
582
|
-
|
|
583
|
-
|
|
1057
|
+
async getApi() {
|
|
1058
|
+
const auth = await this.getOrCreateAuth();
|
|
1059
|
+
return auth.api;
|
|
584
1060
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
1061
|
+
// ---------------------------------------------------------------------------
|
|
1062
|
+
// Device Flow (CLI browser-based login)
|
|
1063
|
+
//
|
|
1064
|
+
// The device authorization flow (RFC 8628) is now handled entirely by
|
|
1065
|
+
// better-auth's `device-authorization` plugin. Endpoints are exposed at
|
|
1066
|
+
// `${basePath}/device/{code,token,approve,deny}` and persisted in
|
|
1067
|
+
// `sys_device_code`. Enable via `plugins.deviceAuthorization: true` in
|
|
1068
|
+
// AuthPluginConfig.
|
|
1069
|
+
// ---------------------------------------------------------------------------
|
|
594
1070
|
getPublicConfig() {
|
|
595
1071
|
const socialProviders = [];
|
|
596
1072
|
if (this.config.socialProviders) {
|
|
@@ -610,11 +1086,22 @@ var AuthManager = class {
|
|
|
610
1086
|
socialProviders.push({
|
|
611
1087
|
id,
|
|
612
1088
|
name: nameMap[id] || id.charAt(0).toUpperCase() + id.slice(1),
|
|
613
|
-
enabled: true
|
|
1089
|
+
enabled: true,
|
|
1090
|
+
type: "social"
|
|
614
1091
|
});
|
|
615
1092
|
}
|
|
616
1093
|
}
|
|
617
1094
|
}
|
|
1095
|
+
if (this.config.oidcProviders?.length) {
|
|
1096
|
+
for (const p of this.config.oidcProviders) {
|
|
1097
|
+
socialProviders.push({
|
|
1098
|
+
id: p.providerId,
|
|
1099
|
+
name: p.name ?? p.providerId.charAt(0).toUpperCase() + p.providerId.slice(1),
|
|
1100
|
+
enabled: true,
|
|
1101
|
+
type: "oidc"
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
618
1105
|
const emailPasswordConfig = this.config.emailAndPassword ?? {};
|
|
619
1106
|
const emailPassword = {
|
|
620
1107
|
enabled: emailPasswordConfig.enabled !== false,
|
|
@@ -627,7 +1114,9 @@ var AuthManager = class {
|
|
|
627
1114
|
twoFactor: pluginConfig.twoFactor ?? false,
|
|
628
1115
|
passkeys: pluginConfig.passkeys ?? false,
|
|
629
1116
|
magicLink: pluginConfig.magicLink ?? false,
|
|
630
|
-
organization: pluginConfig.organization ??
|
|
1117
|
+
organization: pluginConfig.organization ?? true,
|
|
1118
|
+
oidcProvider: pluginConfig.oidcProvider ?? false,
|
|
1119
|
+
deviceAuthorization: pluginConfig.deviceAuthorization ?? false
|
|
631
1120
|
};
|
|
632
1121
|
return {
|
|
633
1122
|
emailPassword,
|
|
@@ -635,776 +1124,50 @@ var AuthManager = class {
|
|
|
635
1124
|
features
|
|
636
1125
|
};
|
|
637
1126
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
label: "User",
|
|
646
|
-
pluralLabel: "Users",
|
|
647
|
-
icon: "user",
|
|
648
|
-
isSystem: true,
|
|
649
|
-
description: "User accounts for authentication",
|
|
650
|
-
titleFormat: "{name} ({email})",
|
|
651
|
-
compactLayout: ["name", "email", "email_verified"],
|
|
652
|
-
fields: {
|
|
653
|
-
id: import_data.Field.text({
|
|
654
|
-
label: "User ID",
|
|
655
|
-
required: true,
|
|
656
|
-
readonly: true
|
|
657
|
-
}),
|
|
658
|
-
created_at: import_data.Field.datetime({
|
|
659
|
-
label: "Created At",
|
|
660
|
-
defaultValue: "NOW()",
|
|
661
|
-
readonly: true
|
|
662
|
-
}),
|
|
663
|
-
updated_at: import_data.Field.datetime({
|
|
664
|
-
label: "Updated At",
|
|
665
|
-
defaultValue: "NOW()",
|
|
666
|
-
readonly: true
|
|
667
|
-
}),
|
|
668
|
-
email: import_data.Field.email({
|
|
669
|
-
label: "Email",
|
|
670
|
-
required: true,
|
|
671
|
-
searchable: true
|
|
672
|
-
}),
|
|
673
|
-
email_verified: import_data.Field.boolean({
|
|
674
|
-
label: "Email Verified",
|
|
675
|
-
defaultValue: false
|
|
676
|
-
}),
|
|
677
|
-
name: import_data.Field.text({
|
|
678
|
-
label: "Name",
|
|
679
|
-
required: true,
|
|
680
|
-
searchable: true,
|
|
681
|
-
maxLength: 255
|
|
682
|
-
}),
|
|
683
|
-
image: import_data.Field.url({
|
|
684
|
-
label: "Profile Image",
|
|
685
|
-
required: false
|
|
686
|
-
})
|
|
687
|
-
},
|
|
688
|
-
indexes: [
|
|
689
|
-
{ fields: ["email"], unique: true },
|
|
690
|
-
{ fields: ["created_at"], unique: false }
|
|
691
|
-
],
|
|
692
|
-
enable: {
|
|
693
|
-
trackHistory: true,
|
|
694
|
-
searchable: true,
|
|
695
|
-
apiEnabled: true,
|
|
696
|
-
apiMethods: ["get", "list", "create", "update", "delete"],
|
|
697
|
-
trash: true,
|
|
698
|
-
mru: true
|
|
699
|
-
},
|
|
700
|
-
validations: [
|
|
701
|
-
{
|
|
702
|
-
name: "email_unique",
|
|
703
|
-
type: "unique",
|
|
704
|
-
severity: "error",
|
|
705
|
-
message: "Email must be unique",
|
|
706
|
-
fields: ["email"],
|
|
707
|
-
caseSensitive: false
|
|
708
|
-
}
|
|
709
|
-
]
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
// src/objects/sys-session.object.ts
|
|
713
|
-
var import_data2 = require("@objectstack/spec/data");
|
|
714
|
-
var SysSession = import_data2.ObjectSchema.create({
|
|
715
|
-
namespace: "sys",
|
|
716
|
-
name: "session",
|
|
717
|
-
label: "Session",
|
|
718
|
-
pluralLabel: "Sessions",
|
|
719
|
-
icon: "key",
|
|
720
|
-
isSystem: true,
|
|
721
|
-
description: "Active user sessions",
|
|
722
|
-
titleFormat: "Session {token}",
|
|
723
|
-
compactLayout: ["user_id", "expires_at", "ip_address"],
|
|
724
|
-
fields: {
|
|
725
|
-
id: import_data2.Field.text({
|
|
726
|
-
label: "Session ID",
|
|
727
|
-
required: true,
|
|
728
|
-
readonly: true
|
|
729
|
-
}),
|
|
730
|
-
created_at: import_data2.Field.datetime({
|
|
731
|
-
label: "Created At",
|
|
732
|
-
defaultValue: "NOW()",
|
|
733
|
-
readonly: true
|
|
734
|
-
}),
|
|
735
|
-
updated_at: import_data2.Field.datetime({
|
|
736
|
-
label: "Updated At",
|
|
737
|
-
defaultValue: "NOW()",
|
|
738
|
-
readonly: true
|
|
739
|
-
}),
|
|
740
|
-
user_id: import_data2.Field.text({
|
|
741
|
-
label: "User ID",
|
|
742
|
-
required: true
|
|
743
|
-
}),
|
|
744
|
-
expires_at: import_data2.Field.datetime({
|
|
745
|
-
label: "Expires At",
|
|
746
|
-
required: true
|
|
747
|
-
}),
|
|
748
|
-
token: import_data2.Field.text({
|
|
749
|
-
label: "Session Token",
|
|
750
|
-
required: true
|
|
751
|
-
}),
|
|
752
|
-
ip_address: import_data2.Field.text({
|
|
753
|
-
label: "IP Address",
|
|
754
|
-
required: false,
|
|
755
|
-
maxLength: 45
|
|
756
|
-
// Support IPv6
|
|
757
|
-
}),
|
|
758
|
-
user_agent: import_data2.Field.textarea({
|
|
759
|
-
label: "User Agent",
|
|
760
|
-
required: false
|
|
761
|
-
})
|
|
762
|
-
},
|
|
763
|
-
indexes: [
|
|
764
|
-
{ fields: ["token"], unique: true },
|
|
765
|
-
{ fields: ["user_id"], unique: false },
|
|
766
|
-
{ fields: ["expires_at"], unique: false }
|
|
767
|
-
],
|
|
768
|
-
enable: {
|
|
769
|
-
trackHistory: false,
|
|
770
|
-
searchable: false,
|
|
771
|
-
apiEnabled: true,
|
|
772
|
-
apiMethods: ["get", "list", "create", "delete"],
|
|
773
|
-
trash: false,
|
|
774
|
-
mru: false
|
|
775
|
-
}
|
|
776
|
-
});
|
|
777
|
-
|
|
778
|
-
// src/objects/sys-account.object.ts
|
|
779
|
-
var import_data3 = require("@objectstack/spec/data");
|
|
780
|
-
var SysAccount = import_data3.ObjectSchema.create({
|
|
781
|
-
namespace: "sys",
|
|
782
|
-
name: "account",
|
|
783
|
-
label: "Account",
|
|
784
|
-
pluralLabel: "Accounts",
|
|
785
|
-
icon: "link",
|
|
786
|
-
isSystem: true,
|
|
787
|
-
description: "OAuth and authentication provider accounts",
|
|
788
|
-
titleFormat: "{provider_id} - {account_id}",
|
|
789
|
-
compactLayout: ["provider_id", "user_id", "account_id"],
|
|
790
|
-
fields: {
|
|
791
|
-
id: import_data3.Field.text({
|
|
792
|
-
label: "Account ID",
|
|
793
|
-
required: true,
|
|
794
|
-
readonly: true
|
|
795
|
-
}),
|
|
796
|
-
created_at: import_data3.Field.datetime({
|
|
797
|
-
label: "Created At",
|
|
798
|
-
defaultValue: "NOW()",
|
|
799
|
-
readonly: true
|
|
800
|
-
}),
|
|
801
|
-
updated_at: import_data3.Field.datetime({
|
|
802
|
-
label: "Updated At",
|
|
803
|
-
defaultValue: "NOW()",
|
|
804
|
-
readonly: true
|
|
805
|
-
}),
|
|
806
|
-
provider_id: import_data3.Field.text({
|
|
807
|
-
label: "Provider ID",
|
|
808
|
-
required: true,
|
|
809
|
-
description: "OAuth provider identifier (google, github, etc.)"
|
|
810
|
-
}),
|
|
811
|
-
account_id: import_data3.Field.text({
|
|
812
|
-
label: "Provider Account ID",
|
|
813
|
-
required: true,
|
|
814
|
-
description: "User's ID in the provider's system"
|
|
815
|
-
}),
|
|
816
|
-
user_id: import_data3.Field.text({
|
|
817
|
-
label: "User ID",
|
|
818
|
-
required: true,
|
|
819
|
-
description: "Link to user table"
|
|
820
|
-
}),
|
|
821
|
-
access_token: import_data3.Field.textarea({
|
|
822
|
-
label: "Access Token",
|
|
823
|
-
required: false
|
|
824
|
-
}),
|
|
825
|
-
refresh_token: import_data3.Field.textarea({
|
|
826
|
-
label: "Refresh Token",
|
|
827
|
-
required: false
|
|
828
|
-
}),
|
|
829
|
-
id_token: import_data3.Field.textarea({
|
|
830
|
-
label: "ID Token",
|
|
831
|
-
required: false
|
|
832
|
-
}),
|
|
833
|
-
access_token_expires_at: import_data3.Field.datetime({
|
|
834
|
-
label: "Access Token Expires At",
|
|
835
|
-
required: false
|
|
836
|
-
}),
|
|
837
|
-
refresh_token_expires_at: import_data3.Field.datetime({
|
|
838
|
-
label: "Refresh Token Expires At",
|
|
839
|
-
required: false
|
|
840
|
-
}),
|
|
841
|
-
scope: import_data3.Field.text({
|
|
842
|
-
label: "OAuth Scope",
|
|
843
|
-
required: false
|
|
844
|
-
}),
|
|
845
|
-
password: import_data3.Field.text({
|
|
846
|
-
label: "Password Hash",
|
|
847
|
-
required: false,
|
|
848
|
-
description: "Hashed password for email/password provider"
|
|
849
|
-
})
|
|
850
|
-
},
|
|
851
|
-
indexes: [
|
|
852
|
-
{ fields: ["user_id"], unique: false },
|
|
853
|
-
{ fields: ["provider_id", "account_id"], unique: true }
|
|
854
|
-
],
|
|
855
|
-
enable: {
|
|
856
|
-
trackHistory: false,
|
|
857
|
-
searchable: false,
|
|
858
|
-
apiEnabled: true,
|
|
859
|
-
apiMethods: ["get", "list", "create", "update", "delete"],
|
|
860
|
-
trash: true,
|
|
861
|
-
mru: false
|
|
862
|
-
}
|
|
863
|
-
});
|
|
864
|
-
|
|
865
|
-
// src/objects/sys-verification.object.ts
|
|
866
|
-
var import_data4 = require("@objectstack/spec/data");
|
|
867
|
-
var SysVerification = import_data4.ObjectSchema.create({
|
|
868
|
-
namespace: "sys",
|
|
869
|
-
name: "verification",
|
|
870
|
-
label: "Verification",
|
|
871
|
-
pluralLabel: "Verifications",
|
|
872
|
-
icon: "shield-check",
|
|
873
|
-
isSystem: true,
|
|
874
|
-
description: "Email and phone verification tokens",
|
|
875
|
-
titleFormat: "Verification for {identifier}",
|
|
876
|
-
compactLayout: ["identifier", "expires_at", "created_at"],
|
|
877
|
-
fields: {
|
|
878
|
-
id: import_data4.Field.text({
|
|
879
|
-
label: "Verification ID",
|
|
880
|
-
required: true,
|
|
881
|
-
readonly: true
|
|
882
|
-
}),
|
|
883
|
-
created_at: import_data4.Field.datetime({
|
|
884
|
-
label: "Created At",
|
|
885
|
-
defaultValue: "NOW()",
|
|
886
|
-
readonly: true
|
|
887
|
-
}),
|
|
888
|
-
updated_at: import_data4.Field.datetime({
|
|
889
|
-
label: "Updated At",
|
|
890
|
-
defaultValue: "NOW()",
|
|
891
|
-
readonly: true
|
|
892
|
-
}),
|
|
893
|
-
value: import_data4.Field.text({
|
|
894
|
-
label: "Verification Token",
|
|
895
|
-
required: true,
|
|
896
|
-
description: "Token or code for verification"
|
|
897
|
-
}),
|
|
898
|
-
expires_at: import_data4.Field.datetime({
|
|
899
|
-
label: "Expires At",
|
|
900
|
-
required: true
|
|
901
|
-
}),
|
|
902
|
-
identifier: import_data4.Field.text({
|
|
903
|
-
label: "Identifier",
|
|
904
|
-
required: true,
|
|
905
|
-
description: "Email address or phone number"
|
|
906
|
-
})
|
|
907
|
-
},
|
|
908
|
-
indexes: [
|
|
909
|
-
{ fields: ["value"], unique: true },
|
|
910
|
-
{ fields: ["identifier"], unique: false },
|
|
911
|
-
{ fields: ["expires_at"], unique: false }
|
|
912
|
-
],
|
|
913
|
-
enable: {
|
|
914
|
-
trackHistory: false,
|
|
915
|
-
searchable: false,
|
|
916
|
-
apiEnabled: true,
|
|
917
|
-
apiMethods: ["get", "create", "delete"],
|
|
918
|
-
trash: false,
|
|
919
|
-
mru: false
|
|
920
|
-
}
|
|
921
|
-
});
|
|
922
|
-
|
|
923
|
-
// src/objects/sys-organization.object.ts
|
|
924
|
-
var import_data5 = require("@objectstack/spec/data");
|
|
925
|
-
var SysOrganization = import_data5.ObjectSchema.create({
|
|
926
|
-
namespace: "sys",
|
|
927
|
-
name: "organization",
|
|
928
|
-
label: "Organization",
|
|
929
|
-
pluralLabel: "Organizations",
|
|
930
|
-
icon: "building-2",
|
|
931
|
-
isSystem: true,
|
|
932
|
-
description: "Organizations for multi-tenant grouping",
|
|
933
|
-
titleFormat: "{name}",
|
|
934
|
-
compactLayout: ["name", "slug", "created_at"],
|
|
935
|
-
fields: {
|
|
936
|
-
id: import_data5.Field.text({
|
|
937
|
-
label: "Organization ID",
|
|
938
|
-
required: true,
|
|
939
|
-
readonly: true
|
|
940
|
-
}),
|
|
941
|
-
created_at: import_data5.Field.datetime({
|
|
942
|
-
label: "Created At",
|
|
943
|
-
defaultValue: "NOW()",
|
|
944
|
-
readonly: true
|
|
945
|
-
}),
|
|
946
|
-
updated_at: import_data5.Field.datetime({
|
|
947
|
-
label: "Updated At",
|
|
948
|
-
defaultValue: "NOW()",
|
|
949
|
-
readonly: true
|
|
950
|
-
}),
|
|
951
|
-
name: import_data5.Field.text({
|
|
952
|
-
label: "Name",
|
|
953
|
-
required: true,
|
|
954
|
-
searchable: true,
|
|
955
|
-
maxLength: 255
|
|
956
|
-
}),
|
|
957
|
-
slug: import_data5.Field.text({
|
|
958
|
-
label: "Slug",
|
|
959
|
-
required: false,
|
|
960
|
-
maxLength: 255,
|
|
961
|
-
description: "URL-friendly identifier"
|
|
962
|
-
}),
|
|
963
|
-
logo: import_data5.Field.url({
|
|
964
|
-
label: "Logo",
|
|
965
|
-
required: false
|
|
966
|
-
}),
|
|
967
|
-
metadata: import_data5.Field.textarea({
|
|
968
|
-
label: "Metadata",
|
|
969
|
-
required: false,
|
|
970
|
-
description: "JSON-serialized organization metadata"
|
|
971
|
-
})
|
|
972
|
-
},
|
|
973
|
-
indexes: [
|
|
974
|
-
{ fields: ["slug"], unique: true },
|
|
975
|
-
{ fields: ["name"] }
|
|
976
|
-
],
|
|
977
|
-
enable: {
|
|
978
|
-
trackHistory: true,
|
|
979
|
-
searchable: true,
|
|
980
|
-
apiEnabled: true,
|
|
981
|
-
apiMethods: ["get", "list", "create", "update", "delete"],
|
|
982
|
-
trash: true,
|
|
983
|
-
mru: true
|
|
984
|
-
}
|
|
985
|
-
});
|
|
986
|
-
|
|
987
|
-
// src/objects/sys-member.object.ts
|
|
988
|
-
var import_data6 = require("@objectstack/spec/data");
|
|
989
|
-
var SysMember = import_data6.ObjectSchema.create({
|
|
990
|
-
namespace: "sys",
|
|
991
|
-
name: "member",
|
|
992
|
-
label: "Member",
|
|
993
|
-
pluralLabel: "Members",
|
|
994
|
-
icon: "user-check",
|
|
995
|
-
isSystem: true,
|
|
996
|
-
description: "Organization membership records",
|
|
997
|
-
titleFormat: "{user_id} in {organization_id}",
|
|
998
|
-
compactLayout: ["user_id", "organization_id", "role"],
|
|
999
|
-
fields: {
|
|
1000
|
-
id: import_data6.Field.text({
|
|
1001
|
-
label: "Member ID",
|
|
1002
|
-
required: true,
|
|
1003
|
-
readonly: true
|
|
1004
|
-
}),
|
|
1005
|
-
created_at: import_data6.Field.datetime({
|
|
1006
|
-
label: "Created At",
|
|
1007
|
-
defaultValue: "NOW()",
|
|
1008
|
-
readonly: true
|
|
1009
|
-
}),
|
|
1010
|
-
organization_id: import_data6.Field.text({
|
|
1011
|
-
label: "Organization ID",
|
|
1012
|
-
required: true
|
|
1013
|
-
}),
|
|
1014
|
-
user_id: import_data6.Field.text({
|
|
1015
|
-
label: "User ID",
|
|
1016
|
-
required: true
|
|
1017
|
-
}),
|
|
1018
|
-
role: import_data6.Field.text({
|
|
1019
|
-
label: "Role",
|
|
1020
|
-
required: false,
|
|
1021
|
-
description: "Member role within the organization (e.g. admin, member)",
|
|
1022
|
-
maxLength: 100
|
|
1023
|
-
})
|
|
1024
|
-
},
|
|
1025
|
-
indexes: [
|
|
1026
|
-
{ fields: ["organization_id", "user_id"], unique: true },
|
|
1027
|
-
{ fields: ["user_id"] }
|
|
1028
|
-
],
|
|
1029
|
-
enable: {
|
|
1030
|
-
trackHistory: true,
|
|
1031
|
-
searchable: false,
|
|
1032
|
-
apiEnabled: true,
|
|
1033
|
-
apiMethods: ["get", "list", "create", "update", "delete"],
|
|
1034
|
-
trash: false,
|
|
1035
|
-
mru: false
|
|
1036
|
-
}
|
|
1037
|
-
});
|
|
1038
|
-
|
|
1039
|
-
// src/objects/sys-invitation.object.ts
|
|
1040
|
-
var import_data7 = require("@objectstack/spec/data");
|
|
1041
|
-
var SysInvitation = import_data7.ObjectSchema.create({
|
|
1042
|
-
namespace: "sys",
|
|
1043
|
-
name: "invitation",
|
|
1044
|
-
label: "Invitation",
|
|
1045
|
-
pluralLabel: "Invitations",
|
|
1046
|
-
icon: "mail",
|
|
1047
|
-
isSystem: true,
|
|
1048
|
-
description: "Organization invitations for user onboarding",
|
|
1049
|
-
titleFormat: "Invitation to {organization_id}",
|
|
1050
|
-
compactLayout: ["email", "organization_id", "status"],
|
|
1051
|
-
fields: {
|
|
1052
|
-
id: import_data7.Field.text({
|
|
1053
|
-
label: "Invitation ID",
|
|
1054
|
-
required: true,
|
|
1055
|
-
readonly: true
|
|
1056
|
-
}),
|
|
1057
|
-
created_at: import_data7.Field.datetime({
|
|
1058
|
-
label: "Created At",
|
|
1059
|
-
defaultValue: "NOW()",
|
|
1060
|
-
readonly: true
|
|
1061
|
-
}),
|
|
1062
|
-
organization_id: import_data7.Field.text({
|
|
1063
|
-
label: "Organization ID",
|
|
1064
|
-
required: true
|
|
1065
|
-
}),
|
|
1066
|
-
email: import_data7.Field.email({
|
|
1067
|
-
label: "Email",
|
|
1068
|
-
required: true,
|
|
1069
|
-
description: "Email address of the invited user"
|
|
1070
|
-
}),
|
|
1071
|
-
role: import_data7.Field.text({
|
|
1072
|
-
label: "Role",
|
|
1073
|
-
required: false,
|
|
1074
|
-
maxLength: 100,
|
|
1075
|
-
description: "Role to assign upon acceptance"
|
|
1076
|
-
}),
|
|
1077
|
-
status: import_data7.Field.select(["pending", "accepted", "rejected", "expired", "canceled"], {
|
|
1078
|
-
label: "Status",
|
|
1079
|
-
required: true,
|
|
1080
|
-
defaultValue: "pending"
|
|
1081
|
-
}),
|
|
1082
|
-
inviter_id: import_data7.Field.text({
|
|
1083
|
-
label: "Inviter ID",
|
|
1084
|
-
required: true,
|
|
1085
|
-
description: "User ID of the person who sent the invitation"
|
|
1086
|
-
}),
|
|
1087
|
-
expires_at: import_data7.Field.datetime({
|
|
1088
|
-
label: "Expires At",
|
|
1089
|
-
required: true
|
|
1090
|
-
}),
|
|
1091
|
-
team_id: import_data7.Field.text({
|
|
1092
|
-
label: "Team ID",
|
|
1093
|
-
required: false,
|
|
1094
|
-
description: "Optional team to assign upon acceptance"
|
|
1095
|
-
})
|
|
1096
|
-
},
|
|
1097
|
-
indexes: [
|
|
1098
|
-
{ fields: ["organization_id"] },
|
|
1099
|
-
{ fields: ["email"] },
|
|
1100
|
-
{ fields: ["expires_at"] }
|
|
1101
|
-
],
|
|
1102
|
-
enable: {
|
|
1103
|
-
trackHistory: true,
|
|
1104
|
-
searchable: false,
|
|
1105
|
-
apiEnabled: true,
|
|
1106
|
-
apiMethods: ["get", "list", "create", "update", "delete"],
|
|
1107
|
-
trash: false,
|
|
1108
|
-
mru: false
|
|
1109
|
-
}
|
|
1110
|
-
});
|
|
1111
|
-
|
|
1112
|
-
// src/objects/sys-team.object.ts
|
|
1113
|
-
var import_data8 = require("@objectstack/spec/data");
|
|
1114
|
-
var SysTeam = import_data8.ObjectSchema.create({
|
|
1115
|
-
namespace: "sys",
|
|
1116
|
-
name: "team",
|
|
1117
|
-
label: "Team",
|
|
1118
|
-
pluralLabel: "Teams",
|
|
1119
|
-
icon: "users",
|
|
1120
|
-
isSystem: true,
|
|
1121
|
-
description: "Teams within organizations for fine-grained grouping",
|
|
1122
|
-
titleFormat: "{name}",
|
|
1123
|
-
compactLayout: ["name", "organization_id", "created_at"],
|
|
1124
|
-
fields: {
|
|
1125
|
-
id: import_data8.Field.text({
|
|
1126
|
-
label: "Team ID",
|
|
1127
|
-
required: true,
|
|
1128
|
-
readonly: true
|
|
1129
|
-
}),
|
|
1130
|
-
created_at: import_data8.Field.datetime({
|
|
1131
|
-
label: "Created At",
|
|
1132
|
-
defaultValue: "NOW()",
|
|
1133
|
-
readonly: true
|
|
1134
|
-
}),
|
|
1135
|
-
updated_at: import_data8.Field.datetime({
|
|
1136
|
-
label: "Updated At",
|
|
1137
|
-
defaultValue: "NOW()",
|
|
1138
|
-
readonly: true
|
|
1139
|
-
}),
|
|
1140
|
-
name: import_data8.Field.text({
|
|
1141
|
-
label: "Name",
|
|
1142
|
-
required: true,
|
|
1143
|
-
searchable: true,
|
|
1144
|
-
maxLength: 255
|
|
1145
|
-
}),
|
|
1146
|
-
organization_id: import_data8.Field.text({
|
|
1147
|
-
label: "Organization ID",
|
|
1148
|
-
required: true
|
|
1149
|
-
})
|
|
1150
|
-
},
|
|
1151
|
-
indexes: [
|
|
1152
|
-
{ fields: ["organization_id"] },
|
|
1153
|
-
{ fields: ["name", "organization_id"], unique: true }
|
|
1154
|
-
],
|
|
1155
|
-
enable: {
|
|
1156
|
-
trackHistory: true,
|
|
1157
|
-
searchable: true,
|
|
1158
|
-
apiEnabled: true,
|
|
1159
|
-
apiMethods: ["get", "list", "create", "update", "delete"],
|
|
1160
|
-
trash: true,
|
|
1161
|
-
mru: false
|
|
1162
|
-
}
|
|
1163
|
-
});
|
|
1164
|
-
|
|
1165
|
-
// src/objects/sys-team-member.object.ts
|
|
1166
|
-
var import_data9 = require("@objectstack/spec/data");
|
|
1167
|
-
var SysTeamMember = import_data9.ObjectSchema.create({
|
|
1168
|
-
namespace: "sys",
|
|
1169
|
-
name: "team_member",
|
|
1170
|
-
label: "Team Member",
|
|
1171
|
-
pluralLabel: "Team Members",
|
|
1172
|
-
icon: "user-plus",
|
|
1173
|
-
isSystem: true,
|
|
1174
|
-
description: "Team membership records linking users to teams",
|
|
1175
|
-
titleFormat: "{user_id} in {team_id}",
|
|
1176
|
-
compactLayout: ["user_id", "team_id", "created_at"],
|
|
1177
|
-
fields: {
|
|
1178
|
-
id: import_data9.Field.text({
|
|
1179
|
-
label: "Team Member ID",
|
|
1180
|
-
required: true,
|
|
1181
|
-
readonly: true
|
|
1182
|
-
}),
|
|
1183
|
-
created_at: import_data9.Field.datetime({
|
|
1184
|
-
label: "Created At",
|
|
1185
|
-
defaultValue: "NOW()",
|
|
1186
|
-
readonly: true
|
|
1187
|
-
}),
|
|
1188
|
-
team_id: import_data9.Field.text({
|
|
1189
|
-
label: "Team ID",
|
|
1190
|
-
required: true
|
|
1191
|
-
}),
|
|
1192
|
-
user_id: import_data9.Field.text({
|
|
1193
|
-
label: "User ID",
|
|
1194
|
-
required: true
|
|
1195
|
-
})
|
|
1196
|
-
},
|
|
1197
|
-
indexes: [
|
|
1198
|
-
{ fields: ["team_id", "user_id"], unique: true },
|
|
1199
|
-
{ fields: ["user_id"] }
|
|
1200
|
-
],
|
|
1201
|
-
enable: {
|
|
1202
|
-
trackHistory: true,
|
|
1203
|
-
searchable: false,
|
|
1204
|
-
apiEnabled: true,
|
|
1205
|
-
apiMethods: ["get", "list", "create", "delete"],
|
|
1206
|
-
trash: false,
|
|
1207
|
-
mru: false
|
|
1208
|
-
}
|
|
1209
|
-
});
|
|
1210
|
-
|
|
1211
|
-
// src/objects/sys-api-key.object.ts
|
|
1212
|
-
var import_data10 = require("@objectstack/spec/data");
|
|
1213
|
-
var SysApiKey = import_data10.ObjectSchema.create({
|
|
1214
|
-
namespace: "sys",
|
|
1215
|
-
name: "api_key",
|
|
1216
|
-
label: "API Key",
|
|
1217
|
-
pluralLabel: "API Keys",
|
|
1218
|
-
icon: "key-round",
|
|
1219
|
-
isSystem: true,
|
|
1220
|
-
description: "API keys for programmatic access",
|
|
1221
|
-
titleFormat: "{name}",
|
|
1222
|
-
compactLayout: ["name", "user_id", "expires_at"],
|
|
1223
|
-
fields: {
|
|
1224
|
-
id: import_data10.Field.text({
|
|
1225
|
-
label: "API Key ID",
|
|
1226
|
-
required: true,
|
|
1227
|
-
readonly: true
|
|
1228
|
-
}),
|
|
1229
|
-
created_at: import_data10.Field.datetime({
|
|
1230
|
-
label: "Created At",
|
|
1231
|
-
defaultValue: "NOW()",
|
|
1232
|
-
readonly: true
|
|
1233
|
-
}),
|
|
1234
|
-
updated_at: import_data10.Field.datetime({
|
|
1235
|
-
label: "Updated At",
|
|
1236
|
-
defaultValue: "NOW()",
|
|
1237
|
-
readonly: true
|
|
1238
|
-
}),
|
|
1239
|
-
name: import_data10.Field.text({
|
|
1240
|
-
label: "Name",
|
|
1241
|
-
required: true,
|
|
1242
|
-
maxLength: 255,
|
|
1243
|
-
description: "Human-readable label for the API key"
|
|
1244
|
-
}),
|
|
1245
|
-
key: import_data10.Field.text({
|
|
1246
|
-
label: "Key",
|
|
1247
|
-
required: true,
|
|
1248
|
-
description: "Hashed API key value"
|
|
1249
|
-
}),
|
|
1250
|
-
prefix: import_data10.Field.text({
|
|
1251
|
-
label: "Prefix",
|
|
1252
|
-
required: false,
|
|
1253
|
-
maxLength: 16,
|
|
1254
|
-
description: 'Visible prefix for identifying the key (e.g., "osk_")'
|
|
1255
|
-
}),
|
|
1256
|
-
user_id: import_data10.Field.text({
|
|
1257
|
-
label: "User ID",
|
|
1258
|
-
required: true,
|
|
1259
|
-
description: "Owner user of this API key"
|
|
1260
|
-
}),
|
|
1261
|
-
scopes: import_data10.Field.textarea({
|
|
1262
|
-
label: "Scopes",
|
|
1263
|
-
required: false,
|
|
1264
|
-
description: "JSON array of permission scopes"
|
|
1265
|
-
}),
|
|
1266
|
-
expires_at: import_data10.Field.datetime({
|
|
1267
|
-
label: "Expires At",
|
|
1268
|
-
required: false
|
|
1269
|
-
}),
|
|
1270
|
-
last_used_at: import_data10.Field.datetime({
|
|
1271
|
-
label: "Last Used At",
|
|
1272
|
-
required: false
|
|
1273
|
-
}),
|
|
1274
|
-
revoked: import_data10.Field.boolean({
|
|
1275
|
-
label: "Revoked",
|
|
1276
|
-
defaultValue: false
|
|
1277
|
-
})
|
|
1278
|
-
},
|
|
1279
|
-
indexes: [
|
|
1280
|
-
{ fields: ["key"], unique: true },
|
|
1281
|
-
{ fields: ["user_id"] },
|
|
1282
|
-
{ fields: ["prefix"] }
|
|
1283
|
-
],
|
|
1284
|
-
enable: {
|
|
1285
|
-
trackHistory: true,
|
|
1286
|
-
searchable: false,
|
|
1287
|
-
apiEnabled: true,
|
|
1288
|
-
apiMethods: ["get", "list", "create", "update", "delete"],
|
|
1289
|
-
trash: false,
|
|
1290
|
-
mru: false
|
|
1291
|
-
}
|
|
1292
|
-
});
|
|
1293
|
-
|
|
1294
|
-
// src/objects/sys-two-factor.object.ts
|
|
1295
|
-
var import_data11 = require("@objectstack/spec/data");
|
|
1296
|
-
var SysTwoFactor = import_data11.ObjectSchema.create({
|
|
1297
|
-
namespace: "sys",
|
|
1298
|
-
name: "two_factor",
|
|
1299
|
-
label: "Two Factor",
|
|
1300
|
-
pluralLabel: "Two Factor Credentials",
|
|
1301
|
-
icon: "smartphone",
|
|
1302
|
-
isSystem: true,
|
|
1303
|
-
description: "Two-factor authentication credentials",
|
|
1304
|
-
titleFormat: "Two-factor for {user_id}",
|
|
1305
|
-
compactLayout: ["user_id", "created_at"],
|
|
1306
|
-
fields: {
|
|
1307
|
-
id: import_data11.Field.text({
|
|
1308
|
-
label: "Two Factor ID",
|
|
1309
|
-
required: true,
|
|
1310
|
-
readonly: true
|
|
1311
|
-
}),
|
|
1312
|
-
created_at: import_data11.Field.datetime({
|
|
1313
|
-
label: "Created At",
|
|
1314
|
-
defaultValue: "NOW()",
|
|
1315
|
-
readonly: true
|
|
1316
|
-
}),
|
|
1317
|
-
updated_at: import_data11.Field.datetime({
|
|
1318
|
-
label: "Updated At",
|
|
1319
|
-
defaultValue: "NOW()",
|
|
1320
|
-
readonly: true
|
|
1321
|
-
}),
|
|
1322
|
-
user_id: import_data11.Field.text({
|
|
1323
|
-
label: "User ID",
|
|
1324
|
-
required: true
|
|
1325
|
-
}),
|
|
1326
|
-
secret: import_data11.Field.text({
|
|
1327
|
-
label: "Secret",
|
|
1328
|
-
required: true,
|
|
1329
|
-
description: "TOTP secret key"
|
|
1330
|
-
}),
|
|
1331
|
-
backup_codes: import_data11.Field.textarea({
|
|
1332
|
-
label: "Backup Codes",
|
|
1333
|
-
required: false,
|
|
1334
|
-
description: "JSON-serialized backup recovery codes"
|
|
1335
|
-
})
|
|
1336
|
-
},
|
|
1337
|
-
indexes: [
|
|
1338
|
-
{ fields: ["user_id"], unique: true }
|
|
1339
|
-
],
|
|
1340
|
-
enable: {
|
|
1341
|
-
trackHistory: false,
|
|
1342
|
-
searchable: false,
|
|
1343
|
-
apiEnabled: true,
|
|
1344
|
-
apiMethods: ["get", "create", "update", "delete"],
|
|
1345
|
-
trash: false,
|
|
1346
|
-
mru: false
|
|
1127
|
+
/**
|
|
1128
|
+
* Returns the data engine wired into this auth manager. Used by route
|
|
1129
|
+
* handlers (e.g. bootstrap-status) that need to query identity tables
|
|
1130
|
+
* directly without going through better-auth.
|
|
1131
|
+
*/
|
|
1132
|
+
getDataEngine() {
|
|
1133
|
+
return this.config.dataEngine;
|
|
1347
1134
|
}
|
|
1348
|
-
}
|
|
1135
|
+
};
|
|
1349
1136
|
|
|
1350
|
-
// src/
|
|
1351
|
-
var
|
|
1352
|
-
var
|
|
1137
|
+
// src/manifest.ts
|
|
1138
|
+
var import_identity = require("@objectstack/platform-objects/identity");
|
|
1139
|
+
var AUTH_PLUGIN_ID = "com.objectstack.plugin-auth";
|
|
1140
|
+
var AUTH_PLUGIN_VERSION = "3.0.1";
|
|
1141
|
+
var authIdentityObjects = [
|
|
1142
|
+
import_identity.SysUser,
|
|
1143
|
+
import_identity.SysSession,
|
|
1144
|
+
import_identity.SysAccount,
|
|
1145
|
+
import_identity.SysVerification,
|
|
1146
|
+
import_identity.SysOrganization,
|
|
1147
|
+
import_identity.SysMember,
|
|
1148
|
+
import_identity.SysInvitation,
|
|
1149
|
+
import_identity.SysTeam,
|
|
1150
|
+
import_identity.SysTeamMember,
|
|
1151
|
+
import_identity.SysApiKey,
|
|
1152
|
+
import_identity.SysTwoFactor,
|
|
1153
|
+
import_identity.SysUserPreference,
|
|
1154
|
+
import_identity.SysOauthApplication,
|
|
1155
|
+
import_identity.SysOauthAccessToken,
|
|
1156
|
+
import_identity.SysOauthRefreshToken,
|
|
1157
|
+
import_identity.SysOauthConsent,
|
|
1158
|
+
import_identity.SysJwks,
|
|
1159
|
+
import_identity.SysDeviceCode
|
|
1160
|
+
];
|
|
1161
|
+
var authPluginManifestHeader = {
|
|
1162
|
+
id: AUTH_PLUGIN_ID,
|
|
1353
1163
|
namespace: "sys",
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
description: "
|
|
1360
|
-
|
|
1361
|
-
compactLayout: ["user_id", "key"],
|
|
1362
|
-
fields: {
|
|
1363
|
-
id: import_data12.Field.text({
|
|
1364
|
-
label: "Preference ID",
|
|
1365
|
-
required: true,
|
|
1366
|
-
readonly: true
|
|
1367
|
-
}),
|
|
1368
|
-
created_at: import_data12.Field.datetime({
|
|
1369
|
-
label: "Created At",
|
|
1370
|
-
defaultValue: "NOW()",
|
|
1371
|
-
readonly: true
|
|
1372
|
-
}),
|
|
1373
|
-
updated_at: import_data12.Field.datetime({
|
|
1374
|
-
label: "Updated At",
|
|
1375
|
-
defaultValue: "NOW()",
|
|
1376
|
-
readonly: true
|
|
1377
|
-
}),
|
|
1378
|
-
user_id: import_data12.Field.text({
|
|
1379
|
-
label: "User ID",
|
|
1380
|
-
required: true,
|
|
1381
|
-
maxLength: 255,
|
|
1382
|
-
description: "Owner user of this preference"
|
|
1383
|
-
}),
|
|
1384
|
-
key: import_data12.Field.text({
|
|
1385
|
-
label: "Key",
|
|
1386
|
-
required: true,
|
|
1387
|
-
maxLength: 255,
|
|
1388
|
-
description: "Preference key (e.g., theme, locale, plugin.ai.auto_save)"
|
|
1389
|
-
}),
|
|
1390
|
-
value: import_data12.Field.json({
|
|
1391
|
-
label: "Value",
|
|
1392
|
-
description: "Preference value (any JSON-serializable type)"
|
|
1393
|
-
})
|
|
1394
|
-
},
|
|
1395
|
-
indexes: [
|
|
1396
|
-
{ fields: ["user_id", "key"], unique: true },
|
|
1397
|
-
{ fields: ["user_id"], unique: false }
|
|
1398
|
-
],
|
|
1399
|
-
enable: {
|
|
1400
|
-
trackHistory: false,
|
|
1401
|
-
searchable: false,
|
|
1402
|
-
apiEnabled: true,
|
|
1403
|
-
apiMethods: ["get", "list", "create", "update", "delete"],
|
|
1404
|
-
trash: false,
|
|
1405
|
-
mru: false
|
|
1406
|
-
}
|
|
1407
|
-
});
|
|
1164
|
+
version: AUTH_PLUGIN_VERSION,
|
|
1165
|
+
type: "plugin",
|
|
1166
|
+
scope: "system",
|
|
1167
|
+
defaultDatasource: "cloud",
|
|
1168
|
+
name: "Authentication & Identity Plugin",
|
|
1169
|
+
description: "Core authentication objects for ObjectStack (User, Session, Account, Verification)"
|
|
1170
|
+
};
|
|
1408
1171
|
|
|
1409
1172
|
// src/auth-plugin.ts
|
|
1410
1173
|
var AuthPlugin = class {
|
|
@@ -1435,43 +1198,25 @@ var AuthPlugin = class {
|
|
|
1435
1198
|
});
|
|
1436
1199
|
ctx.registerService("auth", this.authManager);
|
|
1437
1200
|
ctx.getService("manifest").register({
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
]
|
|
1201
|
+
...authPluginManifestHeader,
|
|
1202
|
+
...this.options.manifestDatasource ? { defaultDatasource: this.options.manifestDatasource } : {},
|
|
1203
|
+
objects: authIdentityObjects,
|
|
1204
|
+
// The platform Setup App is a static metadata artifact (lives in
|
|
1205
|
+
// @objectstack/platform-objects/apps). plugin-auth is the natural
|
|
1206
|
+
// owner of its registration since it loads first among the trio
|
|
1207
|
+
// (auth + security + audit) that supplies the underlying objects.
|
|
1208
|
+
apps: [import_apps.SETUP_APP],
|
|
1209
|
+
// List views for each Setup-nav object are defined on the schema
|
|
1210
|
+
// itself via the canonical `listViews` map (e.g.
|
|
1211
|
+
// sys_user.listViews.{all_users,unverified,two_factor}). Registering
|
|
1212
|
+
// top-level views here is the legacy pre-M10.30c pattern — it caused
|
|
1213
|
+
// duplicate "Users"/"Roles"/"Sessions" tabs to appear alongside the
|
|
1214
|
+
// schema-derived ones, sometimes referencing nonexistent fields
|
|
1215
|
+
// (e.g. legacy `users.view` had phone/status/active columns that do
|
|
1216
|
+
// not exist on sys_user). Schema-embedded listViews is the single
|
|
1217
|
+
// source of truth.
|
|
1218
|
+
dashboards: [import_apps.SystemOverviewDashboard, import_apps.SecurityOverviewDashboard]
|
|
1457
1219
|
});
|
|
1458
|
-
try {
|
|
1459
|
-
const setupNav = ctx.getService("setupNav");
|
|
1460
|
-
if (setupNav) {
|
|
1461
|
-
setupNav.contribute({
|
|
1462
|
-
areaId: "area_administration",
|
|
1463
|
-
items: [
|
|
1464
|
-
{ id: "nav_users", type: "object", label: "Users", objectName: "user", icon: "users", order: 10 },
|
|
1465
|
-
{ id: "nav_organizations", type: "object", label: "Organizations", objectName: "organization", icon: "building-2", order: 20 },
|
|
1466
|
-
{ id: "nav_teams", type: "object", label: "Teams", objectName: "team", icon: "users-round", order: 30 },
|
|
1467
|
-
{ id: "nav_api_keys", type: "object", label: "API Keys", objectName: "api_key", icon: "key", order: 40 },
|
|
1468
|
-
{ id: "nav_sessions", type: "object", label: "Sessions", objectName: "session", icon: "monitor", order: 50 }
|
|
1469
|
-
]
|
|
1470
|
-
});
|
|
1471
|
-
ctx.logger.info("Auth navigation items contributed to Setup App");
|
|
1472
|
-
}
|
|
1473
|
-
} catch {
|
|
1474
|
-
}
|
|
1475
1220
|
ctx.logger.info("Auth Plugin initialized successfully");
|
|
1476
1221
|
}
|
|
1477
1222
|
async start(ctx) {
|
|
@@ -1481,6 +1226,17 @@ var AuthPlugin = class {
|
|
|
1481
1226
|
}
|
|
1482
1227
|
if (this.options.registerRoutes) {
|
|
1483
1228
|
ctx.hook("kernel:ready", async () => {
|
|
1229
|
+
if (this.authManager) {
|
|
1230
|
+
try {
|
|
1231
|
+
const emailSvc = ctx.getService("email");
|
|
1232
|
+
if (emailSvc) {
|
|
1233
|
+
this.authManager.setEmailService(emailSvc);
|
|
1234
|
+
ctx.logger.info("Auth: email service wired (transactional mail enabled)");
|
|
1235
|
+
}
|
|
1236
|
+
} catch {
|
|
1237
|
+
ctx.logger.info("Auth: no email service registered \u2014 auth callbacks will log instead of sending");
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1484
1240
|
let httpServer = null;
|
|
1485
1241
|
try {
|
|
1486
1242
|
httpServer = ctx.getService("http-server");
|
|
@@ -1494,7 +1250,8 @@ var AuthPlugin = class {
|
|
|
1494
1250
|
const configuredUrl = this.options.baseUrl || "http://localhost:3000";
|
|
1495
1251
|
const configuredOrigin = new URL(configuredUrl).origin;
|
|
1496
1252
|
const actualUrl = `http://localhost:${actualPort}`;
|
|
1497
|
-
|
|
1253
|
+
const configuredIsLocalhost = configuredOrigin.startsWith("http://localhost");
|
|
1254
|
+
if (configuredIsLocalhost && configuredOrigin !== actualUrl) {
|
|
1498
1255
|
this.authManager.setRuntimeBaseUrl(actualUrl);
|
|
1499
1256
|
ctx.logger.info(
|
|
1500
1257
|
`Auth baseUrl auto-updated to ${actualUrl} (configured: ${configuredUrl})`
|
|
@@ -1552,23 +1309,26 @@ var AuthPlugin = class {
|
|
|
1552
1309
|
);
|
|
1553
1310
|
}
|
|
1554
1311
|
const rawApp = httpServer.getRawApp();
|
|
1555
|
-
rawApp.get(`${basePath}/config`,
|
|
1312
|
+
rawApp.get(`${basePath}/config`, (c) => {
|
|
1556
1313
|
try {
|
|
1557
1314
|
const config = this.authManager.getPublicConfig();
|
|
1558
|
-
return c.json({
|
|
1559
|
-
success: true,
|
|
1560
|
-
data: config
|
|
1561
|
-
});
|
|
1315
|
+
return c.json({ success: true, data: config });
|
|
1562
1316
|
} catch (error) {
|
|
1563
1317
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1318
|
+
return c.json({ success: false, error: { code: "auth_config_error", message: err.message } }, 500);
|
|
1319
|
+
}
|
|
1320
|
+
});
|
|
1321
|
+
rawApp.get(`${basePath}/bootstrap-status`, async (c) => {
|
|
1322
|
+
try {
|
|
1323
|
+
const dataEngine = this.authManager.getDataEngine();
|
|
1324
|
+
if (!dataEngine) {
|
|
1325
|
+
return c.json({ hasOwner: true });
|
|
1326
|
+
}
|
|
1327
|
+
const count = await dataEngine.count("sys_user", {});
|
|
1328
|
+
return c.json({ hasOwner: (count ?? 0) > 0 });
|
|
1329
|
+
} catch (error) {
|
|
1330
|
+
ctx.logger.warn("[AuthPlugin] bootstrap-status check failed; assuming bootstrapped", error);
|
|
1331
|
+
return c.json({ hasOwner: true });
|
|
1572
1332
|
}
|
|
1573
1333
|
});
|
|
1574
1334
|
rawApp.all(`${basePath}/*`, async (c) => {
|
|
@@ -1582,6 +1342,19 @@ var AuthPlugin = class {
|
|
|
1582
1342
|
ctx.logger.error("[AuthPlugin] better-auth returned server error", new Error(`HTTP ${response.status}: (unable to read body)`));
|
|
1583
1343
|
}
|
|
1584
1344
|
}
|
|
1345
|
+
try {
|
|
1346
|
+
const url = c.req.url;
|
|
1347
|
+
if (response.ok && /\/jwks(\?|$)/.test(url)) {
|
|
1348
|
+
const existing = response.headers.get("cache-control");
|
|
1349
|
+
if (!existing) {
|
|
1350
|
+
response.headers.set(
|
|
1351
|
+
"cache-control",
|
|
1352
|
+
"public, max-age=300, stale-while-revalidate=86400"
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
} catch {
|
|
1357
|
+
}
|
|
1585
1358
|
return response;
|
|
1586
1359
|
} catch (error) {
|
|
1587
1360
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -1598,15 +1371,57 @@ var AuthPlugin = class {
|
|
|
1598
1371
|
);
|
|
1599
1372
|
}
|
|
1600
1373
|
});
|
|
1374
|
+
if (this.options.plugins?.oidcProvider) {
|
|
1375
|
+
void this.registerOidcDiscoveryRoutes(rawApp, ctx).catch((error) => {
|
|
1376
|
+
ctx.logger.error("Failed to register OIDC discovery routes", error);
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1601
1379
|
ctx.logger.info(`Auth routes registered: All requests under ${basePath}/* forwarded to better-auth`);
|
|
1602
1380
|
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Mount the OIDC / OAuth 2.0 well-known discovery documents at the root
|
|
1383
|
+
* URL. Required by RFC 8414 §3 and OpenID Connect Discovery 1.0 §4 — the
|
|
1384
|
+
* documents must live at `/.well-known/{oauth-authorization-server,openid-configuration}`
|
|
1385
|
+
* relative to the issuer, not under the auth basePath.
|
|
1386
|
+
*/
|
|
1387
|
+
async registerOidcDiscoveryRoutes(rawApp, ctx) {
|
|
1388
|
+
const auth = await this.authManager.getAuthInstance();
|
|
1389
|
+
const { oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata } = await import("@better-auth/oauth-provider");
|
|
1390
|
+
const authServerHandler = oauthProviderAuthServerMetadata(auth);
|
|
1391
|
+
const openidConfigHandler = oauthProviderOpenIdConfigMetadata(auth);
|
|
1392
|
+
const DISCOVERY_CACHE = "public, max-age=300, stale-while-revalidate=86400";
|
|
1393
|
+
const withDiscoveryCache = async (handler, req) => {
|
|
1394
|
+
const resp = await handler(req);
|
|
1395
|
+
try {
|
|
1396
|
+
if (resp.ok && !resp.headers.get("cache-control")) {
|
|
1397
|
+
resp.headers.set("cache-control", DISCOVERY_CACHE);
|
|
1398
|
+
}
|
|
1399
|
+
} catch {
|
|
1400
|
+
}
|
|
1401
|
+
return resp;
|
|
1402
|
+
};
|
|
1403
|
+
rawApp.get("/.well-known/oauth-authorization-server", (c) => withDiscoveryCache(authServerHandler, c.req.raw));
|
|
1404
|
+
rawApp.get("/.well-known/openid-configuration", (c) => withDiscoveryCache(openidConfigHandler, c.req.raw));
|
|
1405
|
+
ctx.logger.info(
|
|
1406
|
+
"OIDC discovery endpoints mounted at /.well-known/{oauth-authorization-server,openid-configuration}"
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1603
1409
|
};
|
|
1604
1410
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1605
1411
|
0 && (module.exports = {
|
|
1606
1412
|
AUTH_ACCOUNT_CONFIG,
|
|
1413
|
+
AUTH_ADMIN_SESSION_FIELDS,
|
|
1414
|
+
AUTH_ADMIN_USER_FIELDS,
|
|
1415
|
+
AUTH_DEVICE_CODE_SCHEMA,
|
|
1607
1416
|
AUTH_INVITATION_SCHEMA,
|
|
1417
|
+
AUTH_JWKS_SCHEMA,
|
|
1608
1418
|
AUTH_MEMBER_SCHEMA,
|
|
1609
1419
|
AUTH_MODEL_TO_PROTOCOL,
|
|
1420
|
+
AUTH_OAUTH_ACCESS_TOKEN_SCHEMA,
|
|
1421
|
+
AUTH_OAUTH_APPLICATION_SCHEMA,
|
|
1422
|
+
AUTH_OAUTH_CLIENT_SCHEMA,
|
|
1423
|
+
AUTH_OAUTH_CONSENT_SCHEMA,
|
|
1424
|
+
AUTH_OAUTH_REFRESH_TOKEN_SCHEMA,
|
|
1610
1425
|
AUTH_ORGANIZATION_SCHEMA,
|
|
1611
1426
|
AUTH_ORG_SESSION_FIELDS,
|
|
1612
1427
|
AUTH_SESSION_CONFIG,
|
|
@@ -1616,24 +1431,13 @@ var AuthPlugin = class {
|
|
|
1616
1431
|
AUTH_TWO_FACTOR_USER_FIELDS,
|
|
1617
1432
|
AUTH_USER_CONFIG,
|
|
1618
1433
|
AUTH_VERIFICATION_CONFIG,
|
|
1619
|
-
AuthAccount,
|
|
1620
1434
|
AuthManager,
|
|
1621
1435
|
AuthPlugin,
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
SysInvitation,
|
|
1628
|
-
SysMember,
|
|
1629
|
-
SysOrganization,
|
|
1630
|
-
SysSession,
|
|
1631
|
-
SysTeam,
|
|
1632
|
-
SysTeamMember,
|
|
1633
|
-
SysTwoFactor,
|
|
1634
|
-
SysUser,
|
|
1635
|
-
SysUserPreference,
|
|
1636
|
-
SysVerification,
|
|
1436
|
+
buildAdminPluginSchema,
|
|
1437
|
+
buildDeviceAuthorizationPluginSchema,
|
|
1438
|
+
buildJwtPluginSchema,
|
|
1439
|
+
buildOauthProviderPluginSchema,
|
|
1440
|
+
buildOidcProviderPluginSchema,
|
|
1637
1441
|
buildOrganizationPluginSchema,
|
|
1638
1442
|
buildTwoFactorPluginSchema,
|
|
1639
1443
|
createObjectQLAdapter,
|