@stll/auth-model 0.0.1-placeholder.0 → 0.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 +60 -0
- package/dist/contract.d.ts +414 -0
- package/dist/contract.js +535 -0
- package/package.json +53 -3
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# `@stll/auth-model`
|
|
2
|
+
|
|
3
|
+
Portable Better Auth contracts for applications that need identical identity,
|
|
4
|
+
session, account, verification, organization, membership, invitation, and role
|
|
5
|
+
semantics. The package has no database or framework runtime dependency.
|
|
6
|
+
|
|
7
|
+
The parity manifest pins:
|
|
8
|
+
|
|
9
|
+
- the Better Auth version and singular core model names;
|
|
10
|
+
- PostgreSQL, snake-case mappings, singular model names, transactional adapters,
|
|
11
|
+
application-generated string IDs, database-backed session/verification, and
|
|
12
|
+
timezone-aware timestamps;
|
|
13
|
+
- separate logical and physical field semantics: required/not-null, defaults,
|
|
14
|
+
update behavior, input/output ownership, sorting, indexes, uniqueness,
|
|
15
|
+
references, physical names/types, primary keys, and exact index predicates;
|
|
16
|
+
- the organization plugin options and core role grants.
|
|
17
|
+
|
|
18
|
+
Hosts own runtime tables, adapters, secrets, email delivery, lifecycle hooks, and
|
|
19
|
+
product permissions. Every additional field, index, or plugin model must be
|
|
20
|
+
declared explicitly when comparing the normalized host schema.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import {
|
|
24
|
+
BETTER_AUTH_ADAPTER_OPTIONS,
|
|
25
|
+
BETTER_AUTH_ORGANIZATION_OPTIONS,
|
|
26
|
+
BETTER_AUTH_ORGANIZATION_ROLE_GRANTS,
|
|
27
|
+
BETTER_AUTH_ORGANIZATION_STATEMENTS,
|
|
28
|
+
} from "@stll/auth-model";
|
|
29
|
+
import { createAccessControl } from "better-auth/plugins/access";
|
|
30
|
+
|
|
31
|
+
const statements = {
|
|
32
|
+
...BETTER_AUTH_ORGANIZATION_STATEMENTS,
|
|
33
|
+
document: ["read", "create", "update", "delete"],
|
|
34
|
+
} as const;
|
|
35
|
+
const ac = createAccessControl(statements);
|
|
36
|
+
|
|
37
|
+
const owner = ac.newRole({
|
|
38
|
+
...BETTER_AUTH_ORGANIZATION_ROLE_GRANTS.owner,
|
|
39
|
+
document: ["read", "create", "update", "delete"],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const adapterOptions = {
|
|
43
|
+
provider: "pg",
|
|
44
|
+
schema,
|
|
45
|
+
...BETTER_AUTH_ADAPTER_OPTIONS,
|
|
46
|
+
};
|
|
47
|
+
const organizationOptions = {
|
|
48
|
+
...BETTER_AUTH_ORGANIZATION_OPTIONS,
|
|
49
|
+
ac,
|
|
50
|
+
roles: { owner },
|
|
51
|
+
};
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`compareBetterAuthSchema` accepts a normalized schema and an exact extension
|
|
55
|
+
allowlist. It reports `incompatible` for missing core models or fields, semantic
|
|
56
|
+
drift, undeclared additions, and stale extension declarations. Record and index
|
|
57
|
+
declaration order do not affect the result; index column order remains semantic.
|
|
58
|
+
|
|
59
|
+
The package intentionally does not export runtime tables, secrets, email
|
|
60
|
+
implementations, lifecycle hooks, or product-specific resources.
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
//#region src/contract.d.ts
|
|
2
|
+
/** Better Auth version whose logical schema this contract describes. */
|
|
3
|
+
declare const BETTER_AUTH_CONTRACT_VERSION = "1.6.26";
|
|
4
|
+
declare const ORGANIZATION_ROLE_NAMES: readonly ["owner", "admin", "member", "intern", "external"];
|
|
5
|
+
type OrganizationRoleName = (typeof ORGANIZATION_ROLE_NAMES)[number];
|
|
6
|
+
/** Better Auth organization resources. Hosts extend this with product grants. */
|
|
7
|
+
declare const BETTER_AUTH_ORGANIZATION_STATEMENTS: {
|
|
8
|
+
readonly organization: readonly ["update", "delete"];
|
|
9
|
+
readonly member: readonly ["create", "update", "delete"];
|
|
10
|
+
readonly invitation: readonly ["create", "cancel"];
|
|
11
|
+
readonly team: readonly ["create", "update", "delete"];
|
|
12
|
+
readonly ac: readonly ["create", "read", "update", "delete"];
|
|
13
|
+
};
|
|
14
|
+
type OrganizationPermissionMap = { [Resource in keyof typeof BETTER_AUTH_ORGANIZATION_STATEMENTS]: (typeof BETTER_AUTH_ORGANIZATION_STATEMENTS)[Resource][number][]; };
|
|
15
|
+
type BetterAuthOrganizationRoleGrants = Record<OrganizationRoleName, OrganizationPermissionMap>;
|
|
16
|
+
/** Core grants shared by every application; product grants remain host-owned. */
|
|
17
|
+
declare const BETTER_AUTH_ORGANIZATION_ROLE_GRANTS: {
|
|
18
|
+
owner: {
|
|
19
|
+
organization: ("update" | "delete")[];
|
|
20
|
+
member: ("update" | "delete" | "create")[];
|
|
21
|
+
invitation: ("create" | "cancel")[];
|
|
22
|
+
team: ("update" | "delete" | "create")[];
|
|
23
|
+
ac: ("update" | "delete" | "create" | "read")[];
|
|
24
|
+
};
|
|
25
|
+
admin: {
|
|
26
|
+
organization: "update"[];
|
|
27
|
+
member: ("update" | "delete" | "create")[];
|
|
28
|
+
invitation: ("create" | "cancel")[];
|
|
29
|
+
team: ("update" | "delete" | "create")[];
|
|
30
|
+
ac: ("update" | "delete" | "create" | "read")[];
|
|
31
|
+
};
|
|
32
|
+
member: {
|
|
33
|
+
organization: never[];
|
|
34
|
+
member: never[];
|
|
35
|
+
invitation: never[];
|
|
36
|
+
team: never[];
|
|
37
|
+
ac: "read"[];
|
|
38
|
+
};
|
|
39
|
+
intern: {
|
|
40
|
+
organization: never[];
|
|
41
|
+
member: never[];
|
|
42
|
+
invitation: never[];
|
|
43
|
+
team: never[];
|
|
44
|
+
ac: never[];
|
|
45
|
+
};
|
|
46
|
+
external: {
|
|
47
|
+
organization: never[];
|
|
48
|
+
member: never[];
|
|
49
|
+
invitation: never[];
|
|
50
|
+
team: never[];
|
|
51
|
+
ac: never[];
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Values that change Better Auth's organization authorization semantics.
|
|
56
|
+
* Hooks, adapters, email delivery, and product-specific options stay local.
|
|
57
|
+
*/
|
|
58
|
+
declare const BETTER_AUTH_ORGANIZATION_OPTIONS: {
|
|
59
|
+
readonly allowUserToCreateOrganization: true;
|
|
60
|
+
readonly cancelPendingInvitationsOnReInvite: false;
|
|
61
|
+
readonly creatorRole: "owner";
|
|
62
|
+
readonly invitationExpiresIn: number;
|
|
63
|
+
readonly membershipLimit: 500;
|
|
64
|
+
};
|
|
65
|
+
/** Adapter switches which must match in every host. */
|
|
66
|
+
declare const BETTER_AUTH_ADAPTER_OPTIONS: {
|
|
67
|
+
readonly camelCase: false;
|
|
68
|
+
readonly provider: "pg";
|
|
69
|
+
readonly transaction: true;
|
|
70
|
+
readonly usePlural: false;
|
|
71
|
+
};
|
|
72
|
+
declare const BETTER_AUTH_CORE_MODEL_NAMES: readonly ["user", "session", "account", "verification", "organization", "member", "invitation"];
|
|
73
|
+
type BetterAuthCoreModelName = (typeof BETTER_AUTH_CORE_MODEL_NAMES)[number];
|
|
74
|
+
type BetterAuthFieldType = "boolean" | "date" | "string";
|
|
75
|
+
type BetterAuthFieldDefault = {
|
|
76
|
+
kind: "none";
|
|
77
|
+
} | {
|
|
78
|
+
kind: "runtime";
|
|
79
|
+
} | {
|
|
80
|
+
kind: "literal";
|
|
81
|
+
value: boolean | number | string;
|
|
82
|
+
};
|
|
83
|
+
type BetterAuthFieldReference = {
|
|
84
|
+
field: "id";
|
|
85
|
+
model: "organization" | "user";
|
|
86
|
+
onDelete: "cascade" | null;
|
|
87
|
+
};
|
|
88
|
+
type BetterAuthLogicalFieldContract = {
|
|
89
|
+
default: BetterAuthFieldDefault;
|
|
90
|
+
indexed: boolean;
|
|
91
|
+
input: "allowed" | "server-managed";
|
|
92
|
+
onUpdate: boolean;
|
|
93
|
+
reference: BetterAuthFieldReference | null;
|
|
94
|
+
required: boolean;
|
|
95
|
+
returned: boolean;
|
|
96
|
+
sortable: boolean;
|
|
97
|
+
type: BetterAuthFieldType;
|
|
98
|
+
unique: boolean;
|
|
99
|
+
};
|
|
100
|
+
type BetterAuthDatabaseFieldContract = {
|
|
101
|
+
columnName: string;
|
|
102
|
+
columnType: "boolean" | "text" | "timestamptz";
|
|
103
|
+
default: BetterAuthFieldDefault;
|
|
104
|
+
notNull: boolean;
|
|
105
|
+
onUpdate: boolean;
|
|
106
|
+
precision: number | null;
|
|
107
|
+
reference: BetterAuthFieldReference | null;
|
|
108
|
+
storage: "timestamp-with-time-zone" | null;
|
|
109
|
+
unique: boolean;
|
|
110
|
+
};
|
|
111
|
+
/** Better Auth behavior and physical database guarantees, kept separate. */
|
|
112
|
+
type BetterAuthFieldContract = {
|
|
113
|
+
database: BetterAuthDatabaseFieldContract;
|
|
114
|
+
logical: BetterAuthLogicalFieldContract;
|
|
115
|
+
};
|
|
116
|
+
type BetterAuthIndexContract = {
|
|
117
|
+
fields: readonly string[];
|
|
118
|
+
predicate: string | null;
|
|
119
|
+
unique: boolean;
|
|
120
|
+
};
|
|
121
|
+
type BetterAuthModelContract = {
|
|
122
|
+
fields: Record<string, BetterAuthFieldContract>;
|
|
123
|
+
indexes: readonly BetterAuthIndexContract[];
|
|
124
|
+
primaryKey: readonly string[];
|
|
125
|
+
tableName: string;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Normalized Better Auth core plus the organization plugin's shared models and
|
|
129
|
+
* `activeOrganizationId` session extension.
|
|
130
|
+
*/
|
|
131
|
+
declare const BETTER_AUTH_CORE_SCHEMA: {
|
|
132
|
+
readonly user: {
|
|
133
|
+
readonly tableName: "user";
|
|
134
|
+
readonly primaryKey: readonly ["id"];
|
|
135
|
+
readonly fields: Record<string, BetterAuthFieldContract>;
|
|
136
|
+
readonly indexes: readonly [];
|
|
137
|
+
};
|
|
138
|
+
readonly session: {
|
|
139
|
+
readonly tableName: "session";
|
|
140
|
+
readonly primaryKey: readonly ["id"];
|
|
141
|
+
readonly fields: Record<string, BetterAuthFieldContract>;
|
|
142
|
+
readonly indexes: readonly [{
|
|
143
|
+
readonly fields: readonly ["userId", "activeOrganizationId"];
|
|
144
|
+
readonly predicate: null;
|
|
145
|
+
readonly unique: false;
|
|
146
|
+
}];
|
|
147
|
+
};
|
|
148
|
+
readonly account: {
|
|
149
|
+
readonly tableName: "account";
|
|
150
|
+
readonly primaryKey: readonly ["id"];
|
|
151
|
+
readonly fields: Record<string, BetterAuthFieldContract>;
|
|
152
|
+
readonly indexes: readonly [{
|
|
153
|
+
readonly fields: readonly ["userId"];
|
|
154
|
+
readonly predicate: null;
|
|
155
|
+
readonly unique: false;
|
|
156
|
+
}];
|
|
157
|
+
};
|
|
158
|
+
readonly verification: {
|
|
159
|
+
readonly tableName: "verification";
|
|
160
|
+
readonly primaryKey: readonly ["id"];
|
|
161
|
+
readonly fields: Record<string, BetterAuthFieldContract>;
|
|
162
|
+
readonly indexes: readonly [{
|
|
163
|
+
readonly fields: readonly ["identifier"];
|
|
164
|
+
readonly predicate: null;
|
|
165
|
+
readonly unique: false;
|
|
166
|
+
}];
|
|
167
|
+
};
|
|
168
|
+
readonly organization: {
|
|
169
|
+
readonly tableName: "organization";
|
|
170
|
+
readonly primaryKey: readonly ["id"];
|
|
171
|
+
readonly fields: Record<string, BetterAuthFieldContract>;
|
|
172
|
+
readonly indexes: readonly [{
|
|
173
|
+
readonly fields: readonly ["slug"];
|
|
174
|
+
readonly predicate: null;
|
|
175
|
+
readonly unique: true;
|
|
176
|
+
}];
|
|
177
|
+
};
|
|
178
|
+
readonly member: {
|
|
179
|
+
readonly tableName: "member";
|
|
180
|
+
readonly primaryKey: readonly ["id"];
|
|
181
|
+
readonly fields: Record<string, BetterAuthFieldContract>;
|
|
182
|
+
readonly indexes: readonly [{
|
|
183
|
+
readonly fields: readonly ["organizationId"];
|
|
184
|
+
readonly predicate: null;
|
|
185
|
+
readonly unique: false;
|
|
186
|
+
}, {
|
|
187
|
+
readonly fields: readonly ["userId"];
|
|
188
|
+
readonly predicate: null;
|
|
189
|
+
readonly unique: false;
|
|
190
|
+
}, {
|
|
191
|
+
readonly fields: readonly ["organizationId", "userId"];
|
|
192
|
+
readonly predicate: null;
|
|
193
|
+
readonly unique: true;
|
|
194
|
+
}];
|
|
195
|
+
};
|
|
196
|
+
readonly invitation: {
|
|
197
|
+
readonly tableName: "invitation";
|
|
198
|
+
readonly primaryKey: readonly ["id"];
|
|
199
|
+
readonly fields: Record<string, BetterAuthFieldContract>;
|
|
200
|
+
readonly indexes: readonly [{
|
|
201
|
+
readonly fields: readonly ["organizationId"];
|
|
202
|
+
readonly predicate: null;
|
|
203
|
+
readonly unique: false;
|
|
204
|
+
}, {
|
|
205
|
+
readonly fields: readonly ["email"];
|
|
206
|
+
readonly predicate: null;
|
|
207
|
+
readonly unique: false;
|
|
208
|
+
}];
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
type BetterAuthAdapterContract = {
|
|
212
|
+
camelCase: boolean;
|
|
213
|
+
dateStorage: "timestamp-with-time-zone";
|
|
214
|
+
idGeneration: "application-string";
|
|
215
|
+
modelNames: Record<BetterAuthCoreModelName, string>;
|
|
216
|
+
provider: "pg";
|
|
217
|
+
sessionStorage: "database";
|
|
218
|
+
transaction: boolean;
|
|
219
|
+
usePlural: boolean;
|
|
220
|
+
verificationStorage: "database";
|
|
221
|
+
};
|
|
222
|
+
declare const BETTER_AUTH_ADAPTER_INVARIANTS: {
|
|
223
|
+
readonly dateStorage: "timestamp-with-time-zone";
|
|
224
|
+
readonly idGeneration: "application-string";
|
|
225
|
+
readonly sessionStorage: "database";
|
|
226
|
+
readonly verificationStorage: "database";
|
|
227
|
+
readonly modelNames: {
|
|
228
|
+
readonly user: "user";
|
|
229
|
+
readonly session: "session";
|
|
230
|
+
readonly account: "account";
|
|
231
|
+
readonly verification: "verification";
|
|
232
|
+
readonly organization: "organization";
|
|
233
|
+
readonly member: "member";
|
|
234
|
+
readonly invitation: "invitation";
|
|
235
|
+
};
|
|
236
|
+
readonly camelCase: false;
|
|
237
|
+
readonly provider: "pg";
|
|
238
|
+
readonly transaction: true;
|
|
239
|
+
readonly usePlural: false;
|
|
240
|
+
};
|
|
241
|
+
type BetterAuthSchemaCandidate = {
|
|
242
|
+
adapter: BetterAuthAdapterContract;
|
|
243
|
+
models: Record<string, BetterAuthModelContract>;
|
|
244
|
+
};
|
|
245
|
+
type BetterAuthSchemaExtensions = {
|
|
246
|
+
fields?: Partial<Record<BetterAuthCoreModelName, Record<string, BetterAuthFieldContract>>>;
|
|
247
|
+
indexes?: Partial<Record<BetterAuthCoreModelName, readonly BetterAuthIndexContract[]>>;
|
|
248
|
+
models?: readonly string[];
|
|
249
|
+
};
|
|
250
|
+
type BetterAuthSchemaParityIssue = {
|
|
251
|
+
actual: unknown;
|
|
252
|
+
expected: unknown;
|
|
253
|
+
path: string;
|
|
254
|
+
};
|
|
255
|
+
type BetterAuthSchemaParityResult = {
|
|
256
|
+
status: "compatible";
|
|
257
|
+
} | {
|
|
258
|
+
issues: BetterAuthSchemaParityIssue[];
|
|
259
|
+
status: "incompatible";
|
|
260
|
+
};
|
|
261
|
+
/**
|
|
262
|
+
* Compares a normalized host schema with the complete shared core. Extensions
|
|
263
|
+
* are exact allowlists: missing, stale, and undeclared additions all fail.
|
|
264
|
+
*/
|
|
265
|
+
declare const compareBetterAuthSchema: (candidate: BetterAuthSchemaCandidate, extensions?: BetterAuthSchemaExtensions) => BetterAuthSchemaParityResult;
|
|
266
|
+
declare const BETTER_AUTH_PARITY_MANIFEST: {
|
|
267
|
+
readonly adapterInvariants: {
|
|
268
|
+
readonly dateStorage: "timestamp-with-time-zone";
|
|
269
|
+
readonly idGeneration: "application-string";
|
|
270
|
+
readonly sessionStorage: "database";
|
|
271
|
+
readonly verificationStorage: "database";
|
|
272
|
+
readonly modelNames: {
|
|
273
|
+
readonly user: "user";
|
|
274
|
+
readonly session: "session";
|
|
275
|
+
readonly account: "account";
|
|
276
|
+
readonly verification: "verification";
|
|
277
|
+
readonly organization: "organization";
|
|
278
|
+
readonly member: "member";
|
|
279
|
+
readonly invitation: "invitation";
|
|
280
|
+
};
|
|
281
|
+
readonly camelCase: false;
|
|
282
|
+
readonly provider: "pg";
|
|
283
|
+
readonly transaction: true;
|
|
284
|
+
readonly usePlural: false;
|
|
285
|
+
};
|
|
286
|
+
readonly betterAuthVersion: "1.6.26";
|
|
287
|
+
readonly options: {
|
|
288
|
+
readonly allowUserToCreateOrganization: true;
|
|
289
|
+
readonly cancelPendingInvitationsOnReInvite: false;
|
|
290
|
+
readonly creatorRole: "owner";
|
|
291
|
+
readonly invitationExpiresIn: number;
|
|
292
|
+
readonly membershipLimit: 500;
|
|
293
|
+
};
|
|
294
|
+
readonly roleGrants: {
|
|
295
|
+
owner: {
|
|
296
|
+
organization: ("update" | "delete")[];
|
|
297
|
+
member: ("update" | "delete" | "create")[];
|
|
298
|
+
invitation: ("create" | "cancel")[];
|
|
299
|
+
team: ("update" | "delete" | "create")[];
|
|
300
|
+
ac: ("update" | "delete" | "create" | "read")[];
|
|
301
|
+
};
|
|
302
|
+
admin: {
|
|
303
|
+
organization: "update"[];
|
|
304
|
+
member: ("update" | "delete" | "create")[];
|
|
305
|
+
invitation: ("create" | "cancel")[];
|
|
306
|
+
team: ("update" | "delete" | "create")[];
|
|
307
|
+
ac: ("update" | "delete" | "create" | "read")[];
|
|
308
|
+
};
|
|
309
|
+
member: {
|
|
310
|
+
organization: never[];
|
|
311
|
+
member: never[];
|
|
312
|
+
invitation: never[];
|
|
313
|
+
team: never[];
|
|
314
|
+
ac: "read"[];
|
|
315
|
+
};
|
|
316
|
+
intern: {
|
|
317
|
+
organization: never[];
|
|
318
|
+
member: never[];
|
|
319
|
+
invitation: never[];
|
|
320
|
+
team: never[];
|
|
321
|
+
ac: never[];
|
|
322
|
+
};
|
|
323
|
+
external: {
|
|
324
|
+
organization: never[];
|
|
325
|
+
member: never[];
|
|
326
|
+
invitation: never[];
|
|
327
|
+
team: never[];
|
|
328
|
+
ac: never[];
|
|
329
|
+
};
|
|
330
|
+
};
|
|
331
|
+
readonly roleNames: readonly ["owner", "admin", "member", "intern", "external"];
|
|
332
|
+
readonly schema: {
|
|
333
|
+
readonly user: {
|
|
334
|
+
readonly tableName: "user";
|
|
335
|
+
readonly primaryKey: readonly ["id"];
|
|
336
|
+
readonly fields: Record<string, BetterAuthFieldContract>;
|
|
337
|
+
readonly indexes: readonly [];
|
|
338
|
+
};
|
|
339
|
+
readonly session: {
|
|
340
|
+
readonly tableName: "session";
|
|
341
|
+
readonly primaryKey: readonly ["id"];
|
|
342
|
+
readonly fields: Record<string, BetterAuthFieldContract>;
|
|
343
|
+
readonly indexes: readonly [{
|
|
344
|
+
readonly fields: readonly ["userId", "activeOrganizationId"];
|
|
345
|
+
readonly predicate: null;
|
|
346
|
+
readonly unique: false;
|
|
347
|
+
}];
|
|
348
|
+
};
|
|
349
|
+
readonly account: {
|
|
350
|
+
readonly tableName: "account";
|
|
351
|
+
readonly primaryKey: readonly ["id"];
|
|
352
|
+
readonly fields: Record<string, BetterAuthFieldContract>;
|
|
353
|
+
readonly indexes: readonly [{
|
|
354
|
+
readonly fields: readonly ["userId"];
|
|
355
|
+
readonly predicate: null;
|
|
356
|
+
readonly unique: false;
|
|
357
|
+
}];
|
|
358
|
+
};
|
|
359
|
+
readonly verification: {
|
|
360
|
+
readonly tableName: "verification";
|
|
361
|
+
readonly primaryKey: readonly ["id"];
|
|
362
|
+
readonly fields: Record<string, BetterAuthFieldContract>;
|
|
363
|
+
readonly indexes: readonly [{
|
|
364
|
+
readonly fields: readonly ["identifier"];
|
|
365
|
+
readonly predicate: null;
|
|
366
|
+
readonly unique: false;
|
|
367
|
+
}];
|
|
368
|
+
};
|
|
369
|
+
readonly organization: {
|
|
370
|
+
readonly tableName: "organization";
|
|
371
|
+
readonly primaryKey: readonly ["id"];
|
|
372
|
+
readonly fields: Record<string, BetterAuthFieldContract>;
|
|
373
|
+
readonly indexes: readonly [{
|
|
374
|
+
readonly fields: readonly ["slug"];
|
|
375
|
+
readonly predicate: null;
|
|
376
|
+
readonly unique: true;
|
|
377
|
+
}];
|
|
378
|
+
};
|
|
379
|
+
readonly member: {
|
|
380
|
+
readonly tableName: "member";
|
|
381
|
+
readonly primaryKey: readonly ["id"];
|
|
382
|
+
readonly fields: Record<string, BetterAuthFieldContract>;
|
|
383
|
+
readonly indexes: readonly [{
|
|
384
|
+
readonly fields: readonly ["organizationId"];
|
|
385
|
+
readonly predicate: null;
|
|
386
|
+
readonly unique: false;
|
|
387
|
+
}, {
|
|
388
|
+
readonly fields: readonly ["userId"];
|
|
389
|
+
readonly predicate: null;
|
|
390
|
+
readonly unique: false;
|
|
391
|
+
}, {
|
|
392
|
+
readonly fields: readonly ["organizationId", "userId"];
|
|
393
|
+
readonly predicate: null;
|
|
394
|
+
readonly unique: true;
|
|
395
|
+
}];
|
|
396
|
+
};
|
|
397
|
+
readonly invitation: {
|
|
398
|
+
readonly tableName: "invitation";
|
|
399
|
+
readonly primaryKey: readonly ["id"];
|
|
400
|
+
readonly fields: Record<string, BetterAuthFieldContract>;
|
|
401
|
+
readonly indexes: readonly [{
|
|
402
|
+
readonly fields: readonly ["organizationId"];
|
|
403
|
+
readonly predicate: null;
|
|
404
|
+
readonly unique: false;
|
|
405
|
+
}, {
|
|
406
|
+
readonly fields: readonly ["email"];
|
|
407
|
+
readonly predicate: null;
|
|
408
|
+
readonly unique: false;
|
|
409
|
+
}];
|
|
410
|
+
};
|
|
411
|
+
};
|
|
412
|
+
};
|
|
413
|
+
//#endregion
|
|
414
|
+
export { BETTER_AUTH_ADAPTER_INVARIANTS, BETTER_AUTH_ADAPTER_OPTIONS, BETTER_AUTH_CONTRACT_VERSION, BETTER_AUTH_CORE_MODEL_NAMES, BETTER_AUTH_CORE_SCHEMA, BETTER_AUTH_ORGANIZATION_OPTIONS, BETTER_AUTH_ORGANIZATION_ROLE_GRANTS, BETTER_AUTH_ORGANIZATION_STATEMENTS, BETTER_AUTH_PARITY_MANIFEST, BetterAuthAdapterContract, BetterAuthCoreModelName, BetterAuthDatabaseFieldContract, BetterAuthFieldContract, BetterAuthFieldDefault, BetterAuthFieldReference, BetterAuthFieldType, BetterAuthIndexContract, BetterAuthLogicalFieldContract, BetterAuthModelContract, BetterAuthOrganizationRoleGrants, BetterAuthSchemaCandidate, BetterAuthSchemaExtensions, BetterAuthSchemaParityIssue, BetterAuthSchemaParityResult, ORGANIZATION_ROLE_NAMES, OrganizationRoleName, compareBetterAuthSchema };
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
//#region src/contract.ts
|
|
2
|
+
/** Better Auth version whose logical schema this contract describes. */
|
|
3
|
+
const BETTER_AUTH_CONTRACT_VERSION = "1.6.26";
|
|
4
|
+
const ORGANIZATION_ROLE_NAMES = [
|
|
5
|
+
"owner",
|
|
6
|
+
"admin",
|
|
7
|
+
"member",
|
|
8
|
+
"intern",
|
|
9
|
+
"external"
|
|
10
|
+
];
|
|
11
|
+
/** Better Auth organization resources. Hosts extend this with product grants. */
|
|
12
|
+
const BETTER_AUTH_ORGANIZATION_STATEMENTS = {
|
|
13
|
+
organization: ["update", "delete"],
|
|
14
|
+
member: [
|
|
15
|
+
"create",
|
|
16
|
+
"update",
|
|
17
|
+
"delete"
|
|
18
|
+
],
|
|
19
|
+
invitation: ["create", "cancel"],
|
|
20
|
+
team: [
|
|
21
|
+
"create",
|
|
22
|
+
"update",
|
|
23
|
+
"delete"
|
|
24
|
+
],
|
|
25
|
+
ac: [
|
|
26
|
+
"create",
|
|
27
|
+
"read",
|
|
28
|
+
"update",
|
|
29
|
+
"delete"
|
|
30
|
+
]
|
|
31
|
+
};
|
|
32
|
+
/** Core grants shared by every application; product grants remain host-owned. */
|
|
33
|
+
const BETTER_AUTH_ORGANIZATION_ROLE_GRANTS = {
|
|
34
|
+
owner: {
|
|
35
|
+
organization: ["update", "delete"],
|
|
36
|
+
member: [
|
|
37
|
+
"create",
|
|
38
|
+
"update",
|
|
39
|
+
"delete"
|
|
40
|
+
],
|
|
41
|
+
invitation: ["create", "cancel"],
|
|
42
|
+
team: [
|
|
43
|
+
"create",
|
|
44
|
+
"update",
|
|
45
|
+
"delete"
|
|
46
|
+
],
|
|
47
|
+
ac: [
|
|
48
|
+
"create",
|
|
49
|
+
"read",
|
|
50
|
+
"update",
|
|
51
|
+
"delete"
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
admin: {
|
|
55
|
+
organization: ["update"],
|
|
56
|
+
member: [
|
|
57
|
+
"create",
|
|
58
|
+
"update",
|
|
59
|
+
"delete"
|
|
60
|
+
],
|
|
61
|
+
invitation: ["create", "cancel"],
|
|
62
|
+
team: [
|
|
63
|
+
"create",
|
|
64
|
+
"update",
|
|
65
|
+
"delete"
|
|
66
|
+
],
|
|
67
|
+
ac: [
|
|
68
|
+
"create",
|
|
69
|
+
"read",
|
|
70
|
+
"update",
|
|
71
|
+
"delete"
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
member: {
|
|
75
|
+
organization: [],
|
|
76
|
+
member: [],
|
|
77
|
+
invitation: [],
|
|
78
|
+
team: [],
|
|
79
|
+
ac: ["read"]
|
|
80
|
+
},
|
|
81
|
+
intern: {
|
|
82
|
+
organization: [],
|
|
83
|
+
member: [],
|
|
84
|
+
invitation: [],
|
|
85
|
+
team: [],
|
|
86
|
+
ac: []
|
|
87
|
+
},
|
|
88
|
+
external: {
|
|
89
|
+
organization: [],
|
|
90
|
+
member: [],
|
|
91
|
+
invitation: [],
|
|
92
|
+
team: [],
|
|
93
|
+
ac: []
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Values that change Better Auth's organization authorization semantics.
|
|
98
|
+
* Hooks, adapters, email delivery, and product-specific options stay local.
|
|
99
|
+
*/
|
|
100
|
+
const BETTER_AUTH_ORGANIZATION_OPTIONS = {
|
|
101
|
+
allowUserToCreateOrganization: true,
|
|
102
|
+
cancelPendingInvitationsOnReInvite: false,
|
|
103
|
+
creatorRole: "owner",
|
|
104
|
+
invitationExpiresIn: 172800,
|
|
105
|
+
membershipLimit: 500
|
|
106
|
+
};
|
|
107
|
+
/** Adapter switches which must match in every host. */
|
|
108
|
+
const BETTER_AUTH_ADAPTER_OPTIONS = {
|
|
109
|
+
camelCase: false,
|
|
110
|
+
provider: "pg",
|
|
111
|
+
transaction: true,
|
|
112
|
+
usePlural: false
|
|
113
|
+
};
|
|
114
|
+
const BETTER_AUTH_CORE_MODEL_NAMES = [
|
|
115
|
+
"user",
|
|
116
|
+
"session",
|
|
117
|
+
"account",
|
|
118
|
+
"verification",
|
|
119
|
+
"organization",
|
|
120
|
+
"member",
|
|
121
|
+
"invitation"
|
|
122
|
+
];
|
|
123
|
+
const noDefault = { kind: "none" };
|
|
124
|
+
const runtimeDefault = { kind: "runtime" };
|
|
125
|
+
const field = (type, options = {}) => ({
|
|
126
|
+
database: {
|
|
127
|
+
default: options.database?.default ?? noDefault,
|
|
128
|
+
notNull: options.database?.notNull ?? options.required ?? false,
|
|
129
|
+
onUpdate: options.database?.onUpdate ?? false,
|
|
130
|
+
reference: options.database?.reference ?? (options.reference?.onDelete === "cascade" ? options.reference : null),
|
|
131
|
+
storage: type === "date" ? "timestamp-with-time-zone" : null,
|
|
132
|
+
unique: options.database?.unique ?? options.unique ?? false
|
|
133
|
+
},
|
|
134
|
+
logical: {
|
|
135
|
+
default: options.default ?? noDefault,
|
|
136
|
+
indexed: options.indexed ?? false,
|
|
137
|
+
input: options.input ?? "allowed",
|
|
138
|
+
onUpdate: options.onUpdate ?? false,
|
|
139
|
+
reference: options.reference ?? null,
|
|
140
|
+
required: options.required ?? false,
|
|
141
|
+
returned: options.returned ?? true,
|
|
142
|
+
sortable: options.sortable ?? false,
|
|
143
|
+
type,
|
|
144
|
+
unique: options.unique ?? false
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
const toSnakeCase = (value) => value.replaceAll(/[A-Z]/gu, (letter) => `_${letter.toLowerCase()}`);
|
|
148
|
+
const databaseColumnType = (type) => {
|
|
149
|
+
if (type === "date") return "timestamptz";
|
|
150
|
+
if (type === "boolean") return "boolean";
|
|
151
|
+
return "text";
|
|
152
|
+
};
|
|
153
|
+
const withPhysicalNames = (drafts) => {
|
|
154
|
+
const result = {};
|
|
155
|
+
for (const [name, draft] of Object.entries(drafts)) {
|
|
156
|
+
const type = draft.logical.type;
|
|
157
|
+
result[name] = {
|
|
158
|
+
...draft,
|
|
159
|
+
database: {
|
|
160
|
+
...draft.database,
|
|
161
|
+
columnName: toSnakeCase(name),
|
|
162
|
+
columnType: databaseColumnType(type),
|
|
163
|
+
precision: null
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return result;
|
|
168
|
+
};
|
|
169
|
+
const idField = field("string", {
|
|
170
|
+
default: runtimeDefault,
|
|
171
|
+
input: "server-managed",
|
|
172
|
+
required: true
|
|
173
|
+
});
|
|
174
|
+
const runtimeDate = field("date", {
|
|
175
|
+
default: runtimeDefault,
|
|
176
|
+
required: true,
|
|
177
|
+
database: { default: runtimeDefault }
|
|
178
|
+
});
|
|
179
|
+
const runtimeUpdatedDate = field("date", {
|
|
180
|
+
default: runtimeDefault,
|
|
181
|
+
onUpdate: true,
|
|
182
|
+
required: true,
|
|
183
|
+
database: {
|
|
184
|
+
default: runtimeDefault,
|
|
185
|
+
onUpdate: true
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
const updatedDate = field("date", {
|
|
189
|
+
onUpdate: true,
|
|
190
|
+
required: true,
|
|
191
|
+
database: { onUpdate: true }
|
|
192
|
+
});
|
|
193
|
+
const userReference = {
|
|
194
|
+
field: "id",
|
|
195
|
+
model: "user",
|
|
196
|
+
onDelete: "cascade"
|
|
197
|
+
};
|
|
198
|
+
const organizationReference = {
|
|
199
|
+
field: "id",
|
|
200
|
+
model: "organization",
|
|
201
|
+
onDelete: null
|
|
202
|
+
};
|
|
203
|
+
const organizationDatabaseReference = {
|
|
204
|
+
...organizationReference,
|
|
205
|
+
onDelete: "cascade"
|
|
206
|
+
};
|
|
207
|
+
const userPluginReference = {
|
|
208
|
+
...userReference,
|
|
209
|
+
onDelete: null
|
|
210
|
+
};
|
|
211
|
+
/**
|
|
212
|
+
* Normalized Better Auth core plus the organization plugin's shared models and
|
|
213
|
+
* `activeOrganizationId` session extension.
|
|
214
|
+
*/
|
|
215
|
+
const BETTER_AUTH_CORE_SCHEMA = {
|
|
216
|
+
user: {
|
|
217
|
+
tableName: "user",
|
|
218
|
+
primaryKey: ["id"],
|
|
219
|
+
fields: withPhysicalNames({
|
|
220
|
+
id: idField,
|
|
221
|
+
name: field("string", {
|
|
222
|
+
required: true,
|
|
223
|
+
sortable: true
|
|
224
|
+
}),
|
|
225
|
+
email: field("string", {
|
|
226
|
+
required: true,
|
|
227
|
+
sortable: true,
|
|
228
|
+
unique: true
|
|
229
|
+
}),
|
|
230
|
+
emailVerified: field("boolean", {
|
|
231
|
+
default: {
|
|
232
|
+
kind: "literal",
|
|
233
|
+
value: false
|
|
234
|
+
},
|
|
235
|
+
input: "server-managed",
|
|
236
|
+
required: true,
|
|
237
|
+
database: { default: {
|
|
238
|
+
kind: "literal",
|
|
239
|
+
value: false
|
|
240
|
+
} }
|
|
241
|
+
}),
|
|
242
|
+
image: field("string"),
|
|
243
|
+
createdAt: runtimeDate,
|
|
244
|
+
updatedAt: runtimeUpdatedDate
|
|
245
|
+
}),
|
|
246
|
+
indexes: []
|
|
247
|
+
},
|
|
248
|
+
session: {
|
|
249
|
+
tableName: "session",
|
|
250
|
+
primaryKey: ["id"],
|
|
251
|
+
fields: withPhysicalNames({
|
|
252
|
+
id: idField,
|
|
253
|
+
expiresAt: field("date", { required: true }),
|
|
254
|
+
token: field("string", {
|
|
255
|
+
required: true,
|
|
256
|
+
unique: true
|
|
257
|
+
}),
|
|
258
|
+
createdAt: runtimeDate,
|
|
259
|
+
updatedAt: updatedDate,
|
|
260
|
+
ipAddress: field("string"),
|
|
261
|
+
userAgent: field("string"),
|
|
262
|
+
userId: field("string", {
|
|
263
|
+
indexed: true,
|
|
264
|
+
reference: userReference,
|
|
265
|
+
required: true
|
|
266
|
+
}),
|
|
267
|
+
activeOrganizationId: field("string", { input: "server-managed" })
|
|
268
|
+
}),
|
|
269
|
+
indexes: [{
|
|
270
|
+
fields: ["userId", "activeOrganizationId"],
|
|
271
|
+
predicate: null,
|
|
272
|
+
unique: false
|
|
273
|
+
}]
|
|
274
|
+
},
|
|
275
|
+
account: {
|
|
276
|
+
tableName: "account",
|
|
277
|
+
primaryKey: ["id"],
|
|
278
|
+
fields: withPhysicalNames({
|
|
279
|
+
id: idField,
|
|
280
|
+
accountId: field("string", { required: true }),
|
|
281
|
+
providerId: field("string", { required: true }),
|
|
282
|
+
userId: field("string", {
|
|
283
|
+
indexed: true,
|
|
284
|
+
reference: userReference,
|
|
285
|
+
required: true
|
|
286
|
+
}),
|
|
287
|
+
accessToken: field("string", { returned: false }),
|
|
288
|
+
refreshToken: field("string", { returned: false }),
|
|
289
|
+
idToken: field("string", { returned: false }),
|
|
290
|
+
accessTokenExpiresAt: field("date", { returned: false }),
|
|
291
|
+
refreshTokenExpiresAt: field("date", { returned: false }),
|
|
292
|
+
scope: field("string"),
|
|
293
|
+
password: field("string", { returned: false }),
|
|
294
|
+
createdAt: runtimeDate,
|
|
295
|
+
updatedAt: updatedDate
|
|
296
|
+
}),
|
|
297
|
+
indexes: [{
|
|
298
|
+
fields: ["userId"],
|
|
299
|
+
predicate: null,
|
|
300
|
+
unique: false
|
|
301
|
+
}]
|
|
302
|
+
},
|
|
303
|
+
verification: {
|
|
304
|
+
tableName: "verification",
|
|
305
|
+
primaryKey: ["id"],
|
|
306
|
+
fields: withPhysicalNames({
|
|
307
|
+
id: idField,
|
|
308
|
+
identifier: field("string", {
|
|
309
|
+
indexed: true,
|
|
310
|
+
required: true
|
|
311
|
+
}),
|
|
312
|
+
value: field("string", { required: true }),
|
|
313
|
+
expiresAt: field("date", { required: true }),
|
|
314
|
+
createdAt: runtimeDate,
|
|
315
|
+
updatedAt: runtimeUpdatedDate
|
|
316
|
+
}),
|
|
317
|
+
indexes: [{
|
|
318
|
+
fields: ["identifier"],
|
|
319
|
+
predicate: null,
|
|
320
|
+
unique: false
|
|
321
|
+
}]
|
|
322
|
+
},
|
|
323
|
+
organization: {
|
|
324
|
+
tableName: "organization",
|
|
325
|
+
primaryKey: ["id"],
|
|
326
|
+
fields: withPhysicalNames({
|
|
327
|
+
id: idField,
|
|
328
|
+
name: field("string", {
|
|
329
|
+
required: true,
|
|
330
|
+
sortable: true
|
|
331
|
+
}),
|
|
332
|
+
slug: field("string", {
|
|
333
|
+
indexed: true,
|
|
334
|
+
required: true,
|
|
335
|
+
sortable: true,
|
|
336
|
+
unique: true
|
|
337
|
+
}),
|
|
338
|
+
logo: field("string"),
|
|
339
|
+
createdAt: field("date", { required: true }),
|
|
340
|
+
metadata: field("string")
|
|
341
|
+
}),
|
|
342
|
+
indexes: [{
|
|
343
|
+
fields: ["slug"],
|
|
344
|
+
predicate: null,
|
|
345
|
+
unique: true
|
|
346
|
+
}]
|
|
347
|
+
},
|
|
348
|
+
member: {
|
|
349
|
+
tableName: "member",
|
|
350
|
+
primaryKey: ["id"],
|
|
351
|
+
fields: withPhysicalNames({
|
|
352
|
+
id: idField,
|
|
353
|
+
organizationId: field("string", {
|
|
354
|
+
database: { reference: organizationDatabaseReference },
|
|
355
|
+
indexed: true,
|
|
356
|
+
reference: organizationReference,
|
|
357
|
+
required: true
|
|
358
|
+
}),
|
|
359
|
+
userId: field("string", {
|
|
360
|
+
database: { reference: userReference },
|
|
361
|
+
indexed: true,
|
|
362
|
+
reference: userPluginReference,
|
|
363
|
+
required: true
|
|
364
|
+
}),
|
|
365
|
+
role: field("string", {
|
|
366
|
+
default: {
|
|
367
|
+
kind: "literal",
|
|
368
|
+
value: "member"
|
|
369
|
+
},
|
|
370
|
+
required: true,
|
|
371
|
+
sortable: true,
|
|
372
|
+
database: { default: {
|
|
373
|
+
kind: "literal",
|
|
374
|
+
value: "member"
|
|
375
|
+
} }
|
|
376
|
+
}),
|
|
377
|
+
createdAt: field("date", { required: true })
|
|
378
|
+
}),
|
|
379
|
+
indexes: [
|
|
380
|
+
{
|
|
381
|
+
fields: ["organizationId"],
|
|
382
|
+
predicate: null,
|
|
383
|
+
unique: false
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
fields: ["userId"],
|
|
387
|
+
predicate: null,
|
|
388
|
+
unique: false
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
fields: ["organizationId", "userId"],
|
|
392
|
+
predicate: null,
|
|
393
|
+
unique: true
|
|
394
|
+
}
|
|
395
|
+
]
|
|
396
|
+
},
|
|
397
|
+
invitation: {
|
|
398
|
+
tableName: "invitation",
|
|
399
|
+
primaryKey: ["id"],
|
|
400
|
+
fields: withPhysicalNames({
|
|
401
|
+
id: idField,
|
|
402
|
+
organizationId: field("string", {
|
|
403
|
+
database: { reference: organizationDatabaseReference },
|
|
404
|
+
indexed: true,
|
|
405
|
+
reference: organizationReference,
|
|
406
|
+
required: true
|
|
407
|
+
}),
|
|
408
|
+
email: field("string", {
|
|
409
|
+
indexed: true,
|
|
410
|
+
required: true,
|
|
411
|
+
sortable: true
|
|
412
|
+
}),
|
|
413
|
+
role: field("string", { sortable: true }),
|
|
414
|
+
status: field("string", {
|
|
415
|
+
default: {
|
|
416
|
+
kind: "literal",
|
|
417
|
+
value: "pending"
|
|
418
|
+
},
|
|
419
|
+
required: true,
|
|
420
|
+
sortable: true,
|
|
421
|
+
database: { default: {
|
|
422
|
+
kind: "literal",
|
|
423
|
+
value: "pending"
|
|
424
|
+
} }
|
|
425
|
+
}),
|
|
426
|
+
expiresAt: field("date", { required: true }),
|
|
427
|
+
createdAt: runtimeDate,
|
|
428
|
+
inviterId: field("string", {
|
|
429
|
+
database: { reference: userReference },
|
|
430
|
+
reference: userPluginReference,
|
|
431
|
+
required: true
|
|
432
|
+
})
|
|
433
|
+
}),
|
|
434
|
+
indexes: [{
|
|
435
|
+
fields: ["organizationId"],
|
|
436
|
+
predicate: null,
|
|
437
|
+
unique: false
|
|
438
|
+
}, {
|
|
439
|
+
fields: ["email"],
|
|
440
|
+
predicate: null,
|
|
441
|
+
unique: false
|
|
442
|
+
}]
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
const BETTER_AUTH_ADAPTER_INVARIANTS = {
|
|
446
|
+
...BETTER_AUTH_ADAPTER_OPTIONS,
|
|
447
|
+
dateStorage: "timestamp-with-time-zone",
|
|
448
|
+
idGeneration: "application-string",
|
|
449
|
+
sessionStorage: "database",
|
|
450
|
+
verificationStorage: "database",
|
|
451
|
+
modelNames: {
|
|
452
|
+
user: "user",
|
|
453
|
+
session: "session",
|
|
454
|
+
account: "account",
|
|
455
|
+
verification: "verification",
|
|
456
|
+
organization: "organization",
|
|
457
|
+
member: "member",
|
|
458
|
+
invitation: "invitation"
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
const compareCodeUnits = (left, right) => {
|
|
462
|
+
if (left === right) return 0;
|
|
463
|
+
return left < right ? -1 : 1;
|
|
464
|
+
};
|
|
465
|
+
const canonicalize = (value) => {
|
|
466
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
467
|
+
if (value === null || typeof value !== "object") return value;
|
|
468
|
+
return Object.fromEntries(Object.entries(value).toSorted(([left], [right]) => compareCodeUnits(left, right)).map(([key, child]) => [key, canonicalize(child)]));
|
|
469
|
+
};
|
|
470
|
+
const canonical = (value) => JSON.stringify(canonicalize(value));
|
|
471
|
+
const sortIndexes = (indexes) => indexes.toSorted((left, right) => compareCodeUnits(canonical(left), canonical(right)));
|
|
472
|
+
const compare = (issues, path, actual, expected) => {
|
|
473
|
+
if (canonical(actual) === canonical(expected)) return;
|
|
474
|
+
issues.push({
|
|
475
|
+
actual,
|
|
476
|
+
expected,
|
|
477
|
+
path
|
|
478
|
+
});
|
|
479
|
+
};
|
|
480
|
+
/**
|
|
481
|
+
* Compares a normalized host schema with the complete shared core. Extensions
|
|
482
|
+
* are exact allowlists: missing, stale, and undeclared additions all fail.
|
|
483
|
+
*/
|
|
484
|
+
const compareBetterAuthSchema = (candidate, extensions = {}) => {
|
|
485
|
+
const issues = [];
|
|
486
|
+
compare(issues, "adapter", candidate.adapter, BETTER_AUTH_ADAPTER_INVARIANTS);
|
|
487
|
+
const coreModelNames = new Set(BETTER_AUTH_CORE_MODEL_NAMES);
|
|
488
|
+
for (const model of extensions.models ?? []) if (coreModelNames.has(model)) issues.push({
|
|
489
|
+
actual: "core model",
|
|
490
|
+
expected: "new extension model",
|
|
491
|
+
path: `extensions.models.${model}`
|
|
492
|
+
});
|
|
493
|
+
const expectedModelNames = [...BETTER_AUTH_CORE_MODEL_NAMES, ...extensions.models ?? []].toSorted();
|
|
494
|
+
compare(issues, "models", Object.keys(candidate.models).toSorted(), expectedModelNames);
|
|
495
|
+
for (const model of BETTER_AUTH_CORE_MODEL_NAMES) {
|
|
496
|
+
const actual = candidate.models[model];
|
|
497
|
+
const core = BETTER_AUTH_CORE_SCHEMA[model];
|
|
498
|
+
if (!actual) {
|
|
499
|
+
issues.push({
|
|
500
|
+
actual: void 0,
|
|
501
|
+
expected: "core model",
|
|
502
|
+
path: model
|
|
503
|
+
});
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
compare(issues, `${model}.tableName`, actual.tableName, core.tableName);
|
|
507
|
+
compare(issues, `${model}.primaryKey`, actual.primaryKey, core.primaryKey);
|
|
508
|
+
const extensionFields = extensions.fields?.[model] ?? {};
|
|
509
|
+
for (const fieldName of Object.keys(extensionFields)) if (fieldName in core.fields) issues.push({
|
|
510
|
+
actual: "core field",
|
|
511
|
+
expected: "new extension field",
|
|
512
|
+
path: `extensions.${model}.${fieldName}`
|
|
513
|
+
});
|
|
514
|
+
compare(issues, `${model}.fields`, actual.fields, {
|
|
515
|
+
...extensionFields,
|
|
516
|
+
...core.fields
|
|
517
|
+
});
|
|
518
|
+
compare(issues, `${model}.indexes`, sortIndexes(actual.indexes), sortIndexes([...core.indexes, ...extensions.indexes?.[model] ?? []]));
|
|
519
|
+
}
|
|
520
|
+
if (issues.length === 0) return { status: "compatible" };
|
|
521
|
+
return {
|
|
522
|
+
issues,
|
|
523
|
+
status: "incompatible"
|
|
524
|
+
};
|
|
525
|
+
};
|
|
526
|
+
const BETTER_AUTH_PARITY_MANIFEST = {
|
|
527
|
+
adapterInvariants: BETTER_AUTH_ADAPTER_INVARIANTS,
|
|
528
|
+
betterAuthVersion: BETTER_AUTH_CONTRACT_VERSION,
|
|
529
|
+
options: BETTER_AUTH_ORGANIZATION_OPTIONS,
|
|
530
|
+
roleGrants: BETTER_AUTH_ORGANIZATION_ROLE_GRANTS,
|
|
531
|
+
roleNames: ORGANIZATION_ROLE_NAMES,
|
|
532
|
+
schema: BETTER_AUTH_CORE_SCHEMA
|
|
533
|
+
};
|
|
534
|
+
//#endregion
|
|
535
|
+
export { BETTER_AUTH_ADAPTER_INVARIANTS, BETTER_AUTH_ADAPTER_OPTIONS, BETTER_AUTH_CONTRACT_VERSION, BETTER_AUTH_CORE_MODEL_NAMES, BETTER_AUTH_CORE_SCHEMA, BETTER_AUTH_ORGANIZATION_OPTIONS, BETTER_AUTH_ORGANIZATION_ROLE_GRANTS, BETTER_AUTH_ORGANIZATION_STATEMENTS, BETTER_AUTH_PARITY_MANIFEST, ORGANIZATION_ROLE_NAMES, compareBetterAuthSchema };
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/auth-model",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Portable Better Auth core contract and parity checks.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"authentication",
|
|
7
|
+
"better-auth",
|
|
8
|
+
"multitenancy",
|
|
9
|
+
"organizations",
|
|
10
|
+
"typescript"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/stella/stella/tree/main/packages/auth-model",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/stella/stella/issues"
|
|
15
|
+
},
|
|
16
|
+
"license": "Apache-2.0",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/stella/stella.git",
|
|
20
|
+
"directory": "packages/auth-model"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/contract.d.ts",
|
|
31
|
+
"import": "./dist/contract.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"clean": "git clean -xdf dist .cache .turbo node_modules",
|
|
39
|
+
"build": "tsdown",
|
|
40
|
+
"pack:dry-run": "bun pm pack --dry-run",
|
|
41
|
+
"test": "bun test src",
|
|
42
|
+
"typecheck": "bun ../../packages/scripts/src/tsc-native.ts --noEmit",
|
|
43
|
+
"lint": "cd ../.. && bun --bun oxlint -c oxlint.config.ts --report-unused-disable-directives-severity=error --deny-warnings --type-aware packages/auth-model",
|
|
44
|
+
"lint:fix": "cd ../.. && bun --bun oxlint -c oxlint.config.ts --type-aware --fix packages/auth-model",
|
|
45
|
+
"format": "oxfmt .",
|
|
46
|
+
"prepack": "bun run build"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@stll/typescript-config": "0.0.0",
|
|
50
|
+
"better-auth": "1.6.26",
|
|
51
|
+
"bun-types": "1.4.0",
|
|
52
|
+
"tsdown": "0.22.14"
|
|
53
|
+
},
|
|
54
|
+
"main": "./dist/contract.js",
|
|
55
|
+
"types": "./dist/contract.d.ts"
|
|
6
56
|
}
|