@facilis.dev/auth 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +18 -2
- package/dist/api-clients.d.ts +71 -0
- package/dist/api-clients.js +186 -0
- package/dist/crypto.d.ts +31 -0
- package/dist/crypto.js +76 -0
- package/dist/email-verification.d.ts +45 -0
- package/dist/email-verification.js +72 -0
- package/dist/email.d.ts +7 -0
- package/dist/email.js +9 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/password.d.ts +7 -0
- package/dist/password.js +54 -0
- package/dist/roles.d.ts +40 -0
- package/dist/roles.js +46 -0
- package/dist/seed.d.ts +28 -0
- package/dist/seed.js +115 -0
- package/dist/session.d.ts +66 -0
- package/dist/session.js +161 -0
- package/dist/sso/contracts.d.ts +52 -0
- package/dist/sso/contracts.js +8 -0
- package/dist/sso/errors.d.ts +22 -0
- package/dist/sso/errors.js +45 -0
- package/dist/sso/identity.d.ts +41 -0
- package/dist/sso/identity.js +162 -0
- package/dist/sso/index.d.ts +27 -0
- package/dist/sso/index.js +16 -0
- package/dist/sso/login.d.ts +60 -0
- package/dist/sso/login.js +209 -0
- package/dist/sso/oauth2.d.ts +52 -0
- package/dist/sso/oauth2.js +180 -0
- package/dist/sso/oidc.d.ts +60 -0
- package/dist/sso/oidc.js +201 -0
- package/dist/sso/paths.d.ts +14 -0
- package/dist/sso/paths.js +17 -0
- package/dist/sso/pkce.d.ts +6 -0
- package/dist/sso/pkce.js +21 -0
- package/dist/sso/providers.d.ts +100 -0
- package/dist/sso/providers.js +70 -0
- package/dist/sso/saml.d.ts +44 -0
- package/dist/sso/saml.js +159 -0
- package/dist/sso/state.d.ts +34 -0
- package/dist/sso/state.js +91 -0
- package/dist/sso/tables.d.ts +306 -0
- package/dist/sso/tables.js +59 -0
- package/dist/tables.d.ts +769 -0
- package/dist/tables.js +148 -0
- package/package.json +45 -4
- package/src/api-clients.ts +261 -0
- package/src/crypto.ts +101 -0
- package/src/email-verification.ts +107 -0
- package/src/email.ts +9 -0
- package/src/index.ts +63 -0
- package/src/password.ts +74 -0
- package/src/roles.ts +77 -0
- package/src/seed.ts +146 -0
- package/src/session.ts +209 -0
- package/src/sso/contracts.ts +71 -0
- package/src/sso/errors.ts +53 -0
- package/src/sso/identity.ts +220 -0
- package/src/sso/index.ts +105 -0
- package/src/sso/login.ts +296 -0
- package/src/sso/oauth2.ts +235 -0
- package/src/sso/oidc.ts +285 -0
- package/src/sso/paths.ts +21 -0
- package/src/sso/pkce.ts +30 -0
- package/src/sso/providers.ts +176 -0
- package/src/sso/saml.ts +222 -0
- package/src/sso/state.ts +114 -0
- package/src/sso/tables.ts +77 -0
- package/src/tables.ts +214 -0
- package/src/test-support.ts +149 -0
package/src/tables.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import {
|
|
3
|
+
boolean,
|
|
4
|
+
index,
|
|
5
|
+
pgPolicy,
|
|
6
|
+
pgTable,
|
|
7
|
+
primaryKey,
|
|
8
|
+
text,
|
|
9
|
+
timestamp,
|
|
10
|
+
uniqueIndex,
|
|
11
|
+
uuid,
|
|
12
|
+
type PgColumn,
|
|
13
|
+
type PgPolicy,
|
|
14
|
+
} from "drizzle-orm/pg-core";
|
|
15
|
+
|
|
16
|
+
import { rlsForTable, tenantColumn } from "@facilis.dev/db";
|
|
17
|
+
|
|
18
|
+
const usersRls = rlsForTable({ tenantColumn: "tenant_id" });
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Auth tables (SPEC §5.3). Access is two-class (D-136): a table with RLS is reachable by the
|
|
22
|
+
* app role under its policies; a table without RLS is system-only (`facilis_app` has no
|
|
23
|
+
* privilege on it, only the audited system identity can read or write it).
|
|
24
|
+
*
|
|
25
|
+
* `role_permissions` and `roles` are the system-only pair: `role_permissions` is a
|
|
26
|
+
* projection of facilis.config.ts, written only by migrations/seed and never read at runtime
|
|
27
|
+
* (permissions come from the config through `expandRoles`). `sessions` and `user_roles` are
|
|
28
|
+
* app-reachable, so they carry an RLS policy that inherits the parent `users` row's
|
|
29
|
+
* visibility instead of restating tenant isolation.
|
|
30
|
+
*
|
|
31
|
+
* Users carry both policies: restrictive tenant isolation (D-008) AND-ed with a permissive
|
|
32
|
+
* "tenant scope present" policy, since Postgres denies by default when no permissive policy
|
|
33
|
+
* applies. Net effect: non-superusers only reach users of the tenant in `app.tenant_id`.
|
|
34
|
+
*/
|
|
35
|
+
export const users = pgTable(
|
|
36
|
+
"users",
|
|
37
|
+
{
|
|
38
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
39
|
+
email: text("email").notNull().unique(),
|
|
40
|
+
// The registration insert has no session yet; the column takes the deployment tenant
|
|
41
|
+
// from the policy-scope GUC (D-040). Postgres fills it before the RLS check runs.
|
|
42
|
+
tenantId: tenantColumn(),
|
|
43
|
+
passwordHash: text("password_hash"),
|
|
44
|
+
// Service accounts exist only to back an API client. They have no password and every
|
|
45
|
+
// login path rejects them, so the flag is a stored fact the session/SSO code can check.
|
|
46
|
+
isService: boolean("is_service").notNull().default(false),
|
|
47
|
+
// null means "never verified" (D-156): a password account starts here and only the
|
|
48
|
+
// single-use link consumed by the verification route sets the timestamp.
|
|
49
|
+
emailVerifiedAt: timestamp("email_verified_at"),
|
|
50
|
+
createdAt: timestamp("created_at").defaultNow(),
|
|
51
|
+
},
|
|
52
|
+
(table) => [
|
|
53
|
+
// Canonical identity: emails are stored normalized, and the unique lower(email) index makes
|
|
54
|
+
// a case variant rejected by the database too, not only by the app (D-150).
|
|
55
|
+
uniqueIndex("users_email_lower_idx").on(sql`lower(${table.email})`),
|
|
56
|
+
...usersRls.policies,
|
|
57
|
+
pgPolicy("users_tenant_access", {
|
|
58
|
+
for: "all",
|
|
59
|
+
using: sql`nullif(current_setting('app.tenant_id', true), '') IS NOT NULL`,
|
|
60
|
+
withCheck: sql`nullif(current_setting('app.tenant_id', true), '') IS NOT NULL`,
|
|
61
|
+
}),
|
|
62
|
+
],
|
|
63
|
+
).enableRLS();
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Inherits the parent `users` row's visibility instead of restating it (the D-053 pattern):
|
|
67
|
+
* a session or role assignment is reachable exactly when the user it points at is visible in
|
|
68
|
+
* the current tenant scope. This is what keeps the two tables app-reachable — their rows have
|
|
69
|
+
* no tenant column of their own (D-136).
|
|
70
|
+
*/
|
|
71
|
+
function parentUserAccessPolicy(name: string, column: PgColumn): PgPolicy {
|
|
72
|
+
const predicate = sql`exists (select 1 from ${users} u where u.id = ${column})`;
|
|
73
|
+
return pgPolicy(name, { for: "all", using: predicate, withCheck: predicate });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Email verification links (D-156): one active token per user, stored as `sha256(token)`; the raw
|
|
78
|
+
* link exists only in the delivered email. App-scoped like the other auth tables (D-136), and
|
|
79
|
+
* reached through the audited system elevation by the verification route, which has no session.
|
|
80
|
+
*/
|
|
81
|
+
const emailVerificationsRls = rlsForTable({ tenantColumn: "tenant_id" });
|
|
82
|
+
|
|
83
|
+
export const authEmailVerifications = pgTable(
|
|
84
|
+
"auth_email_verifications",
|
|
85
|
+
{
|
|
86
|
+
// sha256(token): a tenant-scoped read of the table cannot be replayed as a verification link.
|
|
87
|
+
id: text("id").primaryKey(),
|
|
88
|
+
userId: uuid("user_id")
|
|
89
|
+
.notNull()
|
|
90
|
+
.references(() => users.id, { onDelete: "cascade" }),
|
|
91
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
92
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
93
|
+
tenantId: tenantColumn(),
|
|
94
|
+
},
|
|
95
|
+
(table) => [
|
|
96
|
+
// One lookup per user for "rotate the outstanding link"; the primary key covers the token.
|
|
97
|
+
index("auth_email_verifications_user_idx").on(table.userId),
|
|
98
|
+
...emailVerificationsRls.policies,
|
|
99
|
+
pgPolicy("auth_email_verifications_tenant_access", {
|
|
100
|
+
for: "all",
|
|
101
|
+
using: sql`nullif(current_setting('app.tenant_id', true), '') IS NOT NULL`,
|
|
102
|
+
withCheck: sql`nullif(current_setting('app.tenant_id', true), '') IS NOT NULL`,
|
|
103
|
+
}),
|
|
104
|
+
],
|
|
105
|
+
).enableRLS();
|
|
106
|
+
|
|
107
|
+
export const sessions = pgTable(
|
|
108
|
+
"sessions",
|
|
109
|
+
{
|
|
110
|
+
// sha256(cookie id): the raw credential never reaches the table (D-144).
|
|
111
|
+
id: text("id").primaryKey(),
|
|
112
|
+
userId: uuid("user_id")
|
|
113
|
+
.notNull()
|
|
114
|
+
.references(() => users.id),
|
|
115
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
116
|
+
},
|
|
117
|
+
(table) => [parentUserAccessPolicy("sessions_user_access", table.userId)],
|
|
118
|
+
).enableRLS();
|
|
119
|
+
|
|
120
|
+
// System-only (no RLS): seeded by the migration, never read by a route (D-136).
|
|
121
|
+
export const roles = pgTable("roles", {
|
|
122
|
+
name: text("name").primaryKey(),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
export const rolePermissions = pgTable(
|
|
126
|
+
"role_permissions",
|
|
127
|
+
{
|
|
128
|
+
role: text("role")
|
|
129
|
+
.notNull()
|
|
130
|
+
.references(() => roles.name),
|
|
131
|
+
permission: text("permission").notNull(),
|
|
132
|
+
},
|
|
133
|
+
(table) => [primaryKey({ columns: [table.role, table.permission] })],
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
export const userRoles = pgTable(
|
|
137
|
+
"user_roles",
|
|
138
|
+
{
|
|
139
|
+
userId: uuid("user_id")
|
|
140
|
+
.notNull()
|
|
141
|
+
.references(() => users.id),
|
|
142
|
+
role: text("role")
|
|
143
|
+
.notNull()
|
|
144
|
+
.references(() => roles.name),
|
|
145
|
+
assignedBy: uuid("assigned_by"),
|
|
146
|
+
assignedAt: timestamp("assigned_at").defaultNow(),
|
|
147
|
+
},
|
|
148
|
+
(table) => [
|
|
149
|
+
primaryKey({ columns: [table.userId, table.role] }),
|
|
150
|
+
parentUserAccessPolicy("user_roles_user_access", table.userId),
|
|
151
|
+
],
|
|
152
|
+
).enableRLS();
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Programmatic API clients (V3a). A client is a service user plus a hashed secret and a fixed
|
|
156
|
+
* role; both new tables are app-scoped through the parent user (D-136), so the RLS boundary
|
|
157
|
+
* stays the `users` table and a client row is reachable exactly when its service user is.
|
|
158
|
+
*/
|
|
159
|
+
export const apiClients = pgTable(
|
|
160
|
+
"api_clients",
|
|
161
|
+
{
|
|
162
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
163
|
+
clientId: text("client_id").notNull().unique(),
|
|
164
|
+
// Only the SHA-256 of the secret is stored; the plaintext leaves the process once, in
|
|
165
|
+
// the createApiClient result, and can never be recovered from the database.
|
|
166
|
+
secretHash: text("secret_hash").notNull(),
|
|
167
|
+
name: text("name").notNull(),
|
|
168
|
+
// The client's fixed role: an issued token carries exactly this role, never the user's
|
|
169
|
+
// interactive roles (a service account has none worth expanding).
|
|
170
|
+
role: text("role").notNull(),
|
|
171
|
+
userId: uuid("user_id")
|
|
172
|
+
.notNull()
|
|
173
|
+
.references(() => users.id),
|
|
174
|
+
createdBy: uuid("created_by"),
|
|
175
|
+
createdAt: timestamp("created_at").defaultNow(),
|
|
176
|
+
revokedAt: timestamp("revoked_at"),
|
|
177
|
+
lastUsedAt: timestamp("last_used_at"),
|
|
178
|
+
},
|
|
179
|
+
(table) => [parentUserAccessPolicy("api_clients_user_access", table.userId)],
|
|
180
|
+
).enableRLS();
|
|
181
|
+
|
|
182
|
+
export const apiTokens = pgTable(
|
|
183
|
+
"api_tokens",
|
|
184
|
+
{
|
|
185
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
186
|
+
// Lookup key for a presented bearer token: SHA-256 of the raw value, never the value.
|
|
187
|
+
tokenHash: text("token_hash").notNull().unique(),
|
|
188
|
+
clientId: uuid("client_id")
|
|
189
|
+
.notNull()
|
|
190
|
+
.references(() => apiClients.id),
|
|
191
|
+
userId: uuid("user_id")
|
|
192
|
+
.notNull()
|
|
193
|
+
.references(() => users.id),
|
|
194
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
195
|
+
createdAt: timestamp("created_at").defaultNow(),
|
|
196
|
+
revokedAt: timestamp("revoked_at"),
|
|
197
|
+
lastUsedAt: timestamp("last_used_at"),
|
|
198
|
+
},
|
|
199
|
+
(table) => [parentUserAccessPolicy("api_tokens_user_access", table.userId)],
|
|
200
|
+
).enableRLS();
|
|
201
|
+
|
|
202
|
+
export type UserRow = typeof users.$inferSelect;
|
|
203
|
+
export type NewUserRow = typeof users.$inferInsert;
|
|
204
|
+
export type SessionRow = typeof sessions.$inferSelect;
|
|
205
|
+
export type RoleRow = typeof roles.$inferSelect;
|
|
206
|
+
export type RolePermissionRow = typeof rolePermissions.$inferSelect;
|
|
207
|
+
export type UserRoleRow = typeof userRoles.$inferSelect;
|
|
208
|
+
export type ApiClientRow = typeof apiClients.$inferSelect;
|
|
209
|
+
export type ApiTokenRow = typeof apiTokens.$inferSelect;
|
|
210
|
+
export type EmailVerificationRow = typeof authEmailVerifications.$inferSelect;
|
|
211
|
+
export type NewEmailVerificationRow = typeof authEmailVerifications.$inferInsert;
|
|
212
|
+
|
|
213
|
+
/** Public projection of a user row: the password hash never leaves @facilis.dev/auth. */
|
|
214
|
+
export type AuthUser = Pick<UserRow, "id" | "email" | "tenantId" | "createdAt">;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { getTableConfig, type PgTable } from "drizzle-orm/pg-core";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
applyStatements,
|
|
5
|
+
createDb,
|
|
6
|
+
ensureAppRole,
|
|
7
|
+
policyCreateSql,
|
|
8
|
+
type FacilisDb,
|
|
9
|
+
} from "@facilis.dev/db";
|
|
10
|
+
|
|
11
|
+
import { authIdentities, authSsoStates } from "./sso/tables.ts";
|
|
12
|
+
import {
|
|
13
|
+
apiClients,
|
|
14
|
+
apiTokens,
|
|
15
|
+
authEmailVerifications,
|
|
16
|
+
rolePermissions,
|
|
17
|
+
roles,
|
|
18
|
+
sessions,
|
|
19
|
+
userRoles,
|
|
20
|
+
users,
|
|
21
|
+
} from "./tables.ts";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Hand-written DDL matching the drizzle tables exactly (no drizzle-kit in CI). Column
|
|
25
|
+
* names, nullability, defaults, keys and references mirror ./tables.ts.
|
|
26
|
+
*/
|
|
27
|
+
export const AUTH_TABLE_STATEMENTS: readonly string[] = [
|
|
28
|
+
`CREATE TABLE IF NOT EXISTS users (
|
|
29
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
30
|
+
email text NOT NULL UNIQUE,
|
|
31
|
+
tenant_id uuid NOT NULL DEFAULT nullif(current_setting('app.tenant_id', true), '')::uuid,
|
|
32
|
+
password_hash text,
|
|
33
|
+
is_service boolean NOT NULL DEFAULT false,
|
|
34
|
+
email_verified_at timestamp,
|
|
35
|
+
created_at timestamp DEFAULT now()
|
|
36
|
+
)`,
|
|
37
|
+
`CREATE TABLE IF NOT EXISTS sessions (
|
|
38
|
+
id text PRIMARY KEY,
|
|
39
|
+
user_id uuid NOT NULL REFERENCES users(id),
|
|
40
|
+
expires_at timestamp NOT NULL
|
|
41
|
+
)`,
|
|
42
|
+
"CREATE TABLE IF NOT EXISTS roles (name text PRIMARY KEY)",
|
|
43
|
+
`CREATE TABLE IF NOT EXISTS role_permissions (
|
|
44
|
+
role text NOT NULL REFERENCES roles(name),
|
|
45
|
+
permission text NOT NULL,
|
|
46
|
+
PRIMARY KEY (role, permission)
|
|
47
|
+
)`,
|
|
48
|
+
`CREATE TABLE IF NOT EXISTS user_roles (
|
|
49
|
+
user_id uuid NOT NULL REFERENCES users(id),
|
|
50
|
+
role text NOT NULL REFERENCES roles(name),
|
|
51
|
+
assigned_by uuid,
|
|
52
|
+
assigned_at timestamp DEFAULT now(),
|
|
53
|
+
PRIMARY KEY (user_id, role)
|
|
54
|
+
)`,
|
|
55
|
+
`CREATE TABLE IF NOT EXISTS api_clients (
|
|
56
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
57
|
+
client_id text NOT NULL UNIQUE,
|
|
58
|
+
secret_hash text NOT NULL,
|
|
59
|
+
name text NOT NULL,
|
|
60
|
+
role text NOT NULL,
|
|
61
|
+
user_id uuid NOT NULL REFERENCES users(id),
|
|
62
|
+
created_by uuid,
|
|
63
|
+
created_at timestamp DEFAULT now(),
|
|
64
|
+
revoked_at timestamp,
|
|
65
|
+
last_used_at timestamp
|
|
66
|
+
)`,
|
|
67
|
+
`CREATE TABLE IF NOT EXISTS api_tokens (
|
|
68
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
69
|
+
token_hash text NOT NULL UNIQUE,
|
|
70
|
+
client_id uuid NOT NULL REFERENCES api_clients(id),
|
|
71
|
+
user_id uuid NOT NULL REFERENCES users(id),
|
|
72
|
+
expires_at timestamp NOT NULL,
|
|
73
|
+
created_at timestamp DEFAULT now(),
|
|
74
|
+
revoked_at timestamp,
|
|
75
|
+
last_used_at timestamp
|
|
76
|
+
)`,
|
|
77
|
+
`CREATE TABLE IF NOT EXISTS auth_identities (
|
|
78
|
+
provider text NOT NULL,
|
|
79
|
+
subject text NOT NULL,
|
|
80
|
+
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
81
|
+
email text NOT NULL,
|
|
82
|
+
tenant_id uuid NOT NULL DEFAULT nullif(current_setting('app.tenant_id', true), '')::uuid,
|
|
83
|
+
created_at timestamp DEFAULT now() NOT NULL,
|
|
84
|
+
last_login_at timestamp DEFAULT now() NOT NULL,
|
|
85
|
+
PRIMARY KEY (provider, subject)
|
|
86
|
+
)`,
|
|
87
|
+
`CREATE TABLE IF NOT EXISTS auth_sso_states (
|
|
88
|
+
state text PRIMARY KEY,
|
|
89
|
+
provider text NOT NULL,
|
|
90
|
+
nonce text,
|
|
91
|
+
code_verifier text,
|
|
92
|
+
request_id text,
|
|
93
|
+
redirect_to text,
|
|
94
|
+
created_at timestamp DEFAULT now() NOT NULL,
|
|
95
|
+
expires_at timestamp NOT NULL,
|
|
96
|
+
tenant_id uuid NOT NULL DEFAULT nullif(current_setting('app.tenant_id', true), '')::uuid
|
|
97
|
+
)`,
|
|
98
|
+
`CREATE TABLE IF NOT EXISTS auth_email_verifications (
|
|
99
|
+
id text PRIMARY KEY,
|
|
100
|
+
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
101
|
+
created_at timestamp DEFAULT now() NOT NULL,
|
|
102
|
+
expires_at timestamp NOT NULL,
|
|
103
|
+
tenant_id uuid NOT NULL DEFAULT nullif(current_setting('app.tenant_id', true), '')::uuid
|
|
104
|
+
)`,
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
const RLS_TABLES: readonly (readonly [string, PgTable])[] = [
|
|
108
|
+
["users", users],
|
|
109
|
+
["sessions", sessions],
|
|
110
|
+
["roles", roles],
|
|
111
|
+
["role_permissions", rolePermissions],
|
|
112
|
+
["user_roles", userRoles],
|
|
113
|
+
["api_clients", apiClients],
|
|
114
|
+
["api_tokens", apiTokens],
|
|
115
|
+
["auth_identities", authIdentities],
|
|
116
|
+
["auth_sso_states", authSsoStates],
|
|
117
|
+
["auth_email_verifications", authEmailVerifications],
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* RLS wiring derived from the same drizzle tables the DDL mirrors, so a new policy (D-136)
|
|
122
|
+
* cannot land in the schema and stay out of the test database. Tables without `enableRLS`
|
|
123
|
+
* (`roles`, `role_permissions`) are system-only and contribute no statement.
|
|
124
|
+
*/
|
|
125
|
+
const AUTH_RLS_STATEMENTS: readonly string[] = RLS_TABLES.flatMap(([name, table]) => {
|
|
126
|
+
const config = getTableConfig(table);
|
|
127
|
+
if (!config.enableRLS) return [];
|
|
128
|
+
const statements = [`ALTER TABLE "${name}" ENABLE ROW LEVEL SECURITY`];
|
|
129
|
+
for (const policy of config.policies) statements.push(policyCreateSql(name, policy));
|
|
130
|
+
return statements;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
let cachedStatements: readonly string[] | undefined;
|
|
134
|
+
|
|
135
|
+
function authSchemaStatements(): readonly string[] {
|
|
136
|
+
cachedStatements ??= [...AUTH_TABLE_STATEMENTS, ...AUTH_RLS_STATEMENTS];
|
|
137
|
+
return cachedStatements;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Boots an in-memory PGlite db with the auth schema, its RLS policies and the app role:
|
|
142
|
+
* `ensureAppRole` then grants only the RLS-enabled tables and revokes the system-only pair.
|
|
143
|
+
*/
|
|
144
|
+
export async function createAuthTestDb(): Promise<FacilisDb> {
|
|
145
|
+
const db = await createDb();
|
|
146
|
+
await applyStatements(db, authSchemaStatements());
|
|
147
|
+
await ensureAppRole(db);
|
|
148
|
+
return db;
|
|
149
|
+
}
|