hazo_auth 10.2.0 → 10.2.2

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 CHANGED
@@ -54,6 +54,16 @@ HAZO_AUTH_OAUTH_KEY_V1=$(openssl rand -base64 32)
54
54
  npm install hazo_secure
55
55
  ```
56
56
 
57
+ ```js
58
+ // 4. Add hazo_secure to serverExternalPackages in next.config.mjs / next.config.js
59
+ // so the bundler leaves the literal import("hazo_secure/crypto") as a native
60
+ // external import. Without this, Google sign-in fails with
61
+ // GoogleTokenStorageUnconfigured even when hazo_secure is installed.
62
+ const nextConfig = {
63
+ serverExternalPackages: ["hazo_secure", /* ...existing entries */],
64
+ };
65
+ ```
66
+
57
67
  **Client Usage:**
58
68
  ```tsx
59
69
  import { requestGoogleScopes } from "hazo_auth/client";
@@ -371,6 +381,29 @@ npx hazo_auth validate # Check your setup and configuration
371
381
  npx hazo_auth --help # Show all commands
372
382
  ```
373
383
 
384
+ #### Dev-only demo account seeder
385
+
386
+ Seed a **login-ready** test user (email pre-verified, assigned to the default
387
+ system scope with a role, no verification email sent) for local development.
388
+ This is **physically incapable of running in production**: it refuses unless the
389
+ resolved env (`HAZO_ENV ?? NODE_ENV`) is non-production **and**
390
+ `HAZO_AUTH_ALLOW_DEMO_SEED=true` is set.
391
+
392
+ ```bash
393
+ # Create (member by default; --admin grants the global-admin role)
394
+ HAZO_AUTH_ALLOW_DEMO_SEED=true npx hazo_auth demo-create --admin --email=demo@example.com
395
+ # or via npm script
396
+ HAZO_AUTH_ALLOW_DEMO_SEED=true npm run demo:create -- --admin
397
+
398
+ # Delete (by email, prefix, or all __demo_* users)
399
+ HAZO_AUTH_ALLOW_DEMO_SEED=true npx hazo_auth demo-delete --email=demo@example.com
400
+ HAZO_AUTH_ALLOW_DEMO_SEED=true npm run demo:delete -- --all
401
+ ```
402
+
403
+ Programmatic equivalents are exported from `hazo_auth/server-lib`:
404
+ `create_demo_user`, `delete_demo_users`, `assert_demo_seed_allowed`,
405
+ `DemoSeedNotAllowed`.
406
+
374
407
  ### Using Zero-Config Page Components (v2.0+)
375
408
 
376
409
  **NEW in v2.0:** All pages are now React Server Components that initialize everything on the server. No configuration, no loading state, no hassle!
@@ -1340,6 +1373,10 @@ Google OAuth adds one new dependency:
1340
1373
  HAZO_AUTH_OAUTH_KEY_V1=$(openssl rand -base64 32)
1341
1374
  ```
1342
1375
  3. Install the optional peer: `npm install hazo_secure`
1376
+ 4. Add `hazo_secure` to `serverExternalPackages` in `next.config.mjs` / `next.config.js`. The
1377
+ service loads encryption via a literal `import("hazo_secure/crypto")`; if the bundler inlines
1378
+ `hazo_secure` instead of treating it as an external import, Google sign-in fails with
1379
+ `GoogleTokenStorageUnconfigured` even when the peer is installed and keys are set.
1343
1380
 
1344
1381
  **Usage:**
1345
1382
 
@@ -13,6 +13,7 @@ const nextConfig = {
13
13
  serverExternalPackages: [
14
14
  "hazo_notify", "argon2",
15
15
  "hazo_core", "hazo_config", // required for Next 16 + Turbopack
16
+ "hazo_secure", // required if using Google API token storage (getGoogleToken)
16
17
  ],
17
18
  };
18
19
  ```
@@ -1138,13 +1139,17 @@ ls app/api/hazo_auth/set_password/route.ts
1138
1139
  Only needed if you use `requestGoogleScopes` / `getGoogleToken` for Google API access beyond sign-in:
1139
1140
 
1140
1141
  1. Install optional peer: `npm install hazo_secure`
1141
- 2. Run migration: `npm run migrate -- migrations/021_hazo_google_oauth_tokens.sql`
1142
- 3. Set env vars:
1142
+ 2. Add `hazo_secure` to `serverExternalPackages` in `next.config.mjs` / `next.config.js`. The
1143
+ service loads encryption via a literal `import("hazo_secure/crypto")`; if the bundler inlines
1144
+ `hazo_secure` rather than leaving it as a native external import, Google sign-in fails with
1145
+ `GoogleTokenStorageUnconfigured` even when the peer is installed and keys are configured.
1146
+ 3. Run migration: `npm run migrate -- migrations/021_hazo_google_oauth_tokens.sql`
1147
+ 4. Set env vars:
1143
1148
  ```env
1144
1149
  HAZO_AUTH_OAUTH_KEY_CURRENT=v1
1145
1150
  HAZO_AUTH_OAUTH_KEY_V1=<base64-32-bytes from: openssl rand -base64 32>
1146
1151
  ```
1147
- 4. Wire the new routes in your Next.js app (same pattern as other hazo_auth routes):
1152
+ 5. Wire the new routes in your Next.js app (same pattern as other hazo_auth routes):
1148
1153
  ```ts
1149
1154
  // app/api/hazo_auth/google/token/route.ts
1150
1155
  export { GET as googleTokenGET, DELETE as googleTokenDELETE } from "hazo_auth/server/routes";
@@ -0,0 +1,72 @@
1
+ // file_description: CLI command handlers for demo account seeding (dev-only)
2
+
3
+ // section: imports
4
+ import { get_hazo_connect_instance } from "../lib/hazo_connect_instance.server.js";
5
+ import {
6
+ create_demo_user,
7
+ delete_demo_users,
8
+ DemoSeedNotAllowed,
9
+ type CreateDemoUserOptions,
10
+ type DeleteDemoUsersOptions,
11
+ } from "../lib/services/demo_account_service.js";
12
+
13
+ // section: create_handler
14
+ /**
15
+ * Creates a login-ready demo user and prints a summary.
16
+ * Exits with code 1 if demo seeding is not allowed or an error occurs.
17
+ */
18
+ export async function handle_demo_create(opts: CreateDemoUserOptions): Promise<void> {
19
+ try {
20
+ const adapter = get_hazo_connect_instance();
21
+ const result = await create_demo_user(adapter, opts);
22
+
23
+ console.log("");
24
+ console.log("=".repeat(60));
25
+ console.log("DEMO USER CREATED");
26
+ console.log("=".repeat(60));
27
+ console.log("");
28
+ console.log(` Email: ${result.email}`);
29
+ console.log(` Password: ${result.password}`);
30
+ console.log(` User ID: ${result.user_id}`);
31
+ console.log("");
32
+ console.log(" ✓ Email pre-verified — account is login-ready.");
33
+ console.log(" ✓ Assigned to the default system scope.");
34
+ if (opts.admin) {
35
+ console.log(" ✓ Admin account — global-admin role assigned.");
36
+ }
37
+ console.log("=".repeat(60));
38
+ console.log("");
39
+ } catch (error) {
40
+ if (error instanceof DemoSeedNotAllowed) {
41
+ console.error(
42
+ "Demo seeding is disabled. Set HAZO_AUTH_ALLOW_DEMO_SEED=true in a non-production env.",
43
+ );
44
+ process.exit(1);
45
+ }
46
+ console.error(error instanceof Error ? error.message : String(error));
47
+ process.exit(1);
48
+ }
49
+ }
50
+
51
+ // section: delete_handler
52
+ /**
53
+ * Deletes demo users matching the given email or prefix(es) and prints a summary.
54
+ * Exits with code 1 if demo seeding is not allowed or an error occurs.
55
+ */
56
+ export async function handle_demo_delete(opts: DeleteDemoUsersOptions): Promise<void> {
57
+ try {
58
+ const adapter = get_hazo_connect_instance();
59
+ const result = await delete_demo_users(adapter, opts);
60
+
61
+ console.log(`✓ Deleted ${result.deleted} demo user(s).`);
62
+ } catch (error) {
63
+ if (error instanceof DemoSeedNotAllowed) {
64
+ console.error(
65
+ "Demo seeding is disabled. Set HAZO_AUTH_ALLOW_DEMO_SEED=true in a non-production env.",
66
+ );
67
+ process.exit(1);
68
+ }
69
+ console.error(error instanceof Error ? error.message : String(error));
70
+ process.exit(1);
71
+ }
72
+ }
@@ -9,6 +9,7 @@ import { handle_init } from "./init.js";
9
9
  import { handle_init_users, show_init_users_help } from "./init_users.js";
10
10
  import { handle_init_db } from "./init_db.js";
11
11
  import { handle_init_permissions, show_init_permissions_help } from "./init_permissions.js";
12
+ import { handle_demo_create, handle_demo_delete } from "./demo.js";
12
13
 
13
14
  // section: constants
14
15
  const VERSION = "1.6.0";
@@ -23,6 +24,8 @@ Commands:
23
24
  init-db Create SQLite database with hazo_auth schema (standalone)
24
25
  init-permissions Create default permissions from config (no user required)
25
26
  init-users Initialize permissions, roles, and super user from config
27
+ demo-create [DEV ONLY] Create a login-ready demo user (needs HAZO_AUTH_ALLOW_DEMO_SEED=true)
28
+ demo-delete [DEV ONLY] Delete demo users by email/prefix (needs HAZO_AUTH_ALLOW_DEMO_SEED=true)
26
29
  validate Check your hazo_auth setup and configuration
27
30
  generate-routes Generate API route files and pages in your project
28
31
  schema Print the canonical SQLite schema SQL
@@ -41,6 +44,8 @@ Examples:
41
44
  npx hazo_auth generate-routes --pages
42
45
  npx hazo_auth generate-routes --all --dir=src/app
43
46
  npx hazo_auth schema
47
+ HAZO_AUTH_ALLOW_DEMO_SEED=true npx hazo_auth demo-create --admin
48
+ HAZO_AUTH_ALLOW_DEMO_SEED=true npx hazo_auth demo-delete --all
44
49
 
45
50
  Documentation:
46
51
  https://github.com/your-repo/hazo_auth/blob/main/SETUP_CHECKLIST.md
@@ -216,6 +221,58 @@ Requires: better-sqlite3 (npm install better-sqlite3)
216
221
  break;
217
222
  }
218
223
 
224
+ case "demo-create": {
225
+ if (help) {
226
+ console.log(`
227
+ hazo_auth demo-create [DEV ONLY]
228
+
229
+ Create a login-ready demo user (email pre-verified, assigned to the default system scope).
230
+ Refuses to run unless the env is non-production AND HAZO_AUTH_ALLOW_DEMO_SEED=true.
231
+
232
+ Options:
233
+ --email=<addr> Email for the demo user (default: __demo_<timestamp>@hazo.test)
234
+ --password=<pw> Password (default: Demo_Pass123!)
235
+ --name=<name> Display name (default: Demo User)
236
+ --admin Grant the global-admin role instead of a plain member role
237
+ `);
238
+ return;
239
+ }
240
+ const demoCreateOpts: { email?: string; password?: string; name?: string; admin?: boolean } = {};
241
+ for (const arg of args) {
242
+ if (arg.startsWith("--email=")) demoCreateOpts.email = arg.replace("--email=", "");
243
+ else if (arg.startsWith("--password=")) demoCreateOpts.password = arg.replace("--password=", "");
244
+ else if (arg.startsWith("--name=")) demoCreateOpts.name = arg.replace("--name=", "");
245
+ else if (arg === "--admin") demoCreateOpts.admin = true;
246
+ }
247
+ await handle_demo_create(demoCreateOpts);
248
+ break;
249
+ }
250
+
251
+ case "demo-delete": {
252
+ if (help) {
253
+ console.log(`
254
+ hazo_auth demo-delete [DEV ONLY]
255
+
256
+ Delete demo users (and their scope rows) by email or prefix. Defaults to the __demo_ prefix.
257
+ Refuses to run unless the env is non-production AND HAZO_AUTH_ALLOW_DEMO_SEED=true.
258
+
259
+ Options:
260
+ --email=<addr> Delete only the user with this exact email
261
+ --prefix=<p> Delete users whose email starts with this prefix (default: __demo_)
262
+ --all Delete all __demo_-prefixed users (the default behaviour)
263
+ `);
264
+ return;
265
+ }
266
+ const demoDeleteOpts: { email?: string; prefixes?: string[] } = {};
267
+ for (const arg of args) {
268
+ if (arg.startsWith("--email=")) demoDeleteOpts.email = arg.replace("--email=", "");
269
+ else if (arg.startsWith("--prefix=")) demoDeleteOpts.prefixes = [arg.replace("--prefix=", "")];
270
+ }
271
+ // --all is a no-op / explicit default (prefixes stays undefined → service defaults to ['__demo_'])
272
+ await handle_demo_delete(demoDeleteOpts);
273
+ break;
274
+ }
275
+
219
276
  case "validate":
220
277
  if (help) {
221
278
  console.log(`
@@ -0,0 +1,227 @@
1
+ // file_description: dev-only service for seeding demo accounts in hazo_auth; physically blocked in production
2
+
3
+ // section: imports
4
+ import type { HazoConnectAdapter } from "hazo_connect";
5
+ import { createCrudService } from "hazo_connect/server";
6
+ import argon2 from "argon2";
7
+ import { randomUUID } from "crypto";
8
+ import { DEFAULT_SYSTEM_SCOPE_ID } from "./scope_service.js";
9
+ import { GLOBAL_ADMIN_PERMISSION } from "../constants.js";
10
+
11
+ // section: errors
12
+
13
+ export class DemoSeedNotAllowed extends Error {
14
+ constructor(message: string) {
15
+ super(message);
16
+ this.name = "DemoSeedNotAllowed";
17
+ }
18
+ }
19
+
20
+ // section: guards
21
+
22
+ /**
23
+ * Throws DemoSeedNotAllowed if the current environment is production
24
+ * or if HAZO_AUTH_ALLOW_DEMO_SEED is not set to "true".
25
+ * This must be called at the top of every exported function in this module.
26
+ */
27
+ export function assert_demo_seed_allowed(): void {
28
+ const env = process.env.HAZO_ENV ?? process.env.NODE_ENV ?? "development";
29
+ if (env === "production") {
30
+ throw new DemoSeedNotAllowed(
31
+ "Demo seeding is disabled. Requires a non-production env AND HAZO_AUTH_ALLOW_DEMO_SEED=true.",
32
+ );
33
+ }
34
+ if (process.env.HAZO_AUTH_ALLOW_DEMO_SEED !== "true") {
35
+ throw new DemoSeedNotAllowed(
36
+ "Demo seeding is disabled. Requires a non-production env AND HAZO_AUTH_ALLOW_DEMO_SEED=true.",
37
+ );
38
+ }
39
+ }
40
+
41
+ // section: types
42
+
43
+ export type CreateDemoUserOptions = {
44
+ email?: string; // default: `__demo_${Date.now()}@hazo.test`
45
+ password?: string; // default: 'Demo_Pass123!'
46
+ name?: string; // default: 'Demo User'
47
+ admin?: boolean; // default: false
48
+ };
49
+
50
+ export type CreateDemoUserResult = {
51
+ user_id: string;
52
+ email: string;
53
+ password: string;
54
+ };
55
+
56
+ export type DeleteDemoUsersOptions = {
57
+ email?: string;
58
+ prefixes?: string[];
59
+ };
60
+
61
+ // section: helpers
62
+
63
+ /**
64
+ * Creates a demo user in the database, assigning them to the default system scope.
65
+ * Idempotent on email — if the user already exists their id is reused and the
66
+ * scope/role assignment is still ensured.
67
+ *
68
+ * REQUIRES: non-production env + HAZO_AUTH_ALLOW_DEMO_SEED=true
69
+ */
70
+ export async function create_demo_user(
71
+ adapter: HazoConnectAdapter,
72
+ opts: CreateDemoUserOptions = {},
73
+ ): Promise<CreateDemoUserResult> {
74
+ assert_demo_seed_allowed();
75
+
76
+ const email = opts.email ?? `__demo_${Date.now()}@hazo.test`;
77
+ const password = opts.password ?? "Demo_Pass123!";
78
+ const name = opts.name ?? "Demo User";
79
+ const admin = opts.admin ?? false;
80
+
81
+ const now = new Date().toISOString();
82
+
83
+ const users_service = createCrudService(adapter, "hazo_users");
84
+ const roles_service = createCrudService(adapter, "hazo_roles");
85
+ const permissions_service = createCrudService(adapter, "hazo_permissions");
86
+ const role_permissions_service = createCrudService(
87
+ adapter,
88
+ "hazo_role_permissions",
89
+ { primaryKeys: ["role_id", "permission_id"], autoId: false },
90
+ );
91
+ const user_scopes_service = createCrudService(adapter, "hazo_user_scopes", {
92
+ primaryKeys: ["user_id", "scope_id"],
93
+ autoId: false,
94
+ });
95
+
96
+ // Idempotent: reuse existing user if email already registered
97
+ let user_id: string;
98
+ const existing_users = await users_service.findBy({ email_address: email });
99
+ if (Array.isArray(existing_users) && existing_users.length > 0) {
100
+ user_id = existing_users[0].id as string;
101
+ } else {
102
+ const password_hash = await argon2.hash(password);
103
+ user_id = randomUUID();
104
+
105
+ await users_service.insert({
106
+ id: user_id,
107
+ email_address: email,
108
+ password_hash,
109
+ email_verified: true,
110
+ status: "ACTIVE",
111
+ login_attempts: 0,
112
+ auth_providers: "email",
113
+ name,
114
+ created_at: now,
115
+ changed_at: now,
116
+ });
117
+ }
118
+
119
+ // Ensure role exists
120
+ const role_name = admin ? "default_super_user_role" : "demo_member_role";
121
+ const existing_roles = await roles_service.findBy({ role_name });
122
+ let role_id: string;
123
+ if (Array.isArray(existing_roles) && existing_roles.length > 0) {
124
+ role_id = existing_roles[0].id as string;
125
+ } else {
126
+ const new_role = await roles_service.insert({ role_name, created_at: now, changed_at: now });
127
+ role_id = Array.isArray(new_role)
128
+ ? (new_role[0] as { id: string }).id
129
+ : (new_role as { id: string }).id;
130
+ }
131
+
132
+ // Admin users: ensure GLOBAL_ADMIN_PERMISSION is in the catalog and linked to the role
133
+ if (admin) {
134
+ // Ensure permission exists
135
+ const existing_perms = await permissions_service.findBy({
136
+ permission_name: GLOBAL_ADMIN_PERMISSION,
137
+ });
138
+ let permission_id: string;
139
+ if (Array.isArray(existing_perms) && existing_perms.length > 0) {
140
+ permission_id = existing_perms[0].id as string;
141
+ } else {
142
+ const new_perm = await permissions_service.insert({
143
+ permission_name: GLOBAL_ADMIN_PERMISSION,
144
+ description: "Global admin — access to all scopes and operations",
145
+ created_at: now,
146
+ changed_at: now,
147
+ });
148
+ permission_id = Array.isArray(new_perm)
149
+ ? (new_perm[0] as { id: string }).id
150
+ : (new_perm as { id: string }).id;
151
+ }
152
+
153
+ // Ensure role→permission link
154
+ const existing_rp = await role_permissions_service.findBy({ role_id, permission_id });
155
+ if (!Array.isArray(existing_rp) || existing_rp.length === 0) {
156
+ await role_permissions_service.insert({ role_id, permission_id });
157
+ }
158
+ }
159
+
160
+ // Ensure user→scope assignment
161
+ const existing_user_scopes = await user_scopes_service.findBy({
162
+ user_id,
163
+ scope_id: DEFAULT_SYSTEM_SCOPE_ID,
164
+ });
165
+ if (!Array.isArray(existing_user_scopes) || existing_user_scopes.length === 0) {
166
+ await user_scopes_service.insert({
167
+ user_id,
168
+ scope_id: DEFAULT_SYSTEM_SCOPE_ID,
169
+ root_scope_id: DEFAULT_SYSTEM_SCOPE_ID,
170
+ role_id,
171
+ created_at: now,
172
+ changed_at: now,
173
+ });
174
+ }
175
+
176
+ return { user_id, email, password };
177
+ }
178
+
179
+ /**
180
+ * Deletes demo users (and their scope rows) whose email matches the given
181
+ * address or any of the given prefixes. Defaults to the "__demo_" prefix.
182
+ *
183
+ * REQUIRES: non-production env + HAZO_AUTH_ALLOW_DEMO_SEED=true
184
+ */
185
+ export async function delete_demo_users(
186
+ adapter: HazoConnectAdapter,
187
+ opts: DeleteDemoUsersOptions = {},
188
+ ): Promise<{ deleted: number }> {
189
+ assert_demo_seed_allowed();
190
+
191
+ const prefixes = opts.prefixes ?? ["__demo_"];
192
+
193
+ const users = createCrudService<{ id: string; email_address: string }>(
194
+ adapter,
195
+ "hazo_users",
196
+ );
197
+ const user_scopes_service = createCrudService(adapter, "hazo_user_scopes", {
198
+ primaryKeys: ["user_id", "scope_id"],
199
+ autoId: false,
200
+ });
201
+
202
+ const all = await users.list((qb) => qb.limit(500));
203
+ const targets = all.filter((u) => {
204
+ if (opts.email && u.email_address === opts.email) return true;
205
+ return prefixes.some(
206
+ (p) => typeof u.email_address === "string" && u.email_address.startsWith(p),
207
+ );
208
+ });
209
+
210
+ let deleted = 0;
211
+ for (const u of targets) {
212
+ const uid = u.id;
213
+ try {
214
+ // Best-effort: delete this user's scope rows. hazo_user_scopes has a
215
+ // composite PK (no `id` column); deleteById on a multi-key table falls
216
+ // back to the first primary key (user_id), so this removes all of the
217
+ // user's scope rows in one call.
218
+ await user_scopes_service.deleteById(uid).catch(() => {});
219
+ await users.deleteById(uid);
220
+ deleted += 1;
221
+ } catch {
222
+ // best-effort — skip users that can't be deleted
223
+ }
224
+ }
225
+
226
+ return { deleted };
227
+ }
@@ -3,7 +3,6 @@
3
3
  import { createCrudService } from "hazo_connect/server";
4
4
  import { get_hazo_connect_instance } from "../hazo_connect_instance.server.js";
5
5
  import { create_app_logger } from "../app_logger.js";
6
- import { optional_import } from "hazo_core";
7
6
  import { randomUUID } from "crypto";
8
7
 
9
8
  // section: errors
@@ -70,10 +69,32 @@ function makeKeyProvider(
70
69
  });
71
70
  }
72
71
 
72
+ /**
73
+ * Indirection around the hazo_secure/crypto dynamic import.
74
+ *
75
+ * The import specifier MUST be a string literal. hazo_secure is an optional
76
+ * peer dep, so this used to go through hazo_core's `optional_import(pkg)` — but
77
+ * that performs a dynamic `import(variable)`, which Turbopack/webpack cannot
78
+ * statically resolve when consumers bundle their server. The import then fails
79
+ * unconditionally and sign-in dies on GoogleTokenStorageUnconfigured even when
80
+ * hazo_secure is installed. A literal `import("hazo_secure/crypto")` is left as
81
+ * a native external import and resolves at runtime (consumers must list
82
+ * hazo_secure in serverExternalPackages so it isn't bundled).
83
+ *
84
+ * Wrapped in an object so tests can stub `_cryptoLoader.load` without touching
85
+ * the literal specifier. Not part of the public API (underscore-prefixed).
86
+ */
87
+ export const _cryptoLoader = {
88
+ load: (): Promise<typeof import("hazo_secure/crypto")> => import("hazo_secure/crypto"),
89
+ };
90
+
73
91
  async function load_crypto_module() {
74
- const cryptoModule = await optional_import<typeof import("hazo_secure/crypto")>(
75
- "hazo_secure/crypto"
76
- );
92
+ let cryptoModule: typeof import("hazo_secure/crypto") | null;
93
+ try {
94
+ cryptoModule = await _cryptoLoader.load();
95
+ } catch {
96
+ cryptoModule = null;
97
+ }
77
98
  if (!cryptoModule) throw new GoogleTokenStorageUnconfigured();
78
99
  return cryptoModule;
79
100
  }
@@ -21,4 +21,5 @@ export * from "./user_scope_service.js";
21
21
  export * from "./oauth_service.js";
22
22
  export * from "./branding_service.js";
23
23
  export * from "./google_token_service.js";
24
+ export * from "./demo_account_service.js";
24
25
 
@@ -0,0 +1,12 @@
1
+ import { type CreateDemoUserOptions, type DeleteDemoUsersOptions } from "../lib/services/demo_account_service.js";
2
+ /**
3
+ * Creates a login-ready demo user and prints a summary.
4
+ * Exits with code 1 if demo seeding is not allowed or an error occurs.
5
+ */
6
+ export declare function handle_demo_create(opts: CreateDemoUserOptions): Promise<void>;
7
+ /**
8
+ * Deletes demo users matching the given email or prefix(es) and prints a summary.
9
+ * Exits with code 1 if demo seeding is not allowed or an error occurs.
10
+ */
11
+ export declare function handle_demo_delete(opts: DeleteDemoUsersOptions): Promise<void>;
12
+ //# sourceMappingURL=demo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"demo.d.ts","sourceRoot":"","sources":["../../src/cli/demo.ts"],"names":[],"mappings":"AAIA,OAAO,EAIL,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC5B,MAAM,yCAAyC,CAAC;AAGjD;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+BnF;AAGD;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBpF"}
@@ -0,0 +1,59 @@
1
+ // file_description: CLI command handlers for demo account seeding (dev-only)
2
+ // section: imports
3
+ import { get_hazo_connect_instance } from "../lib/hazo_connect_instance.server.js";
4
+ import { create_demo_user, delete_demo_users, DemoSeedNotAllowed, } from "../lib/services/demo_account_service.js";
5
+ // section: create_handler
6
+ /**
7
+ * Creates a login-ready demo user and prints a summary.
8
+ * Exits with code 1 if demo seeding is not allowed or an error occurs.
9
+ */
10
+ export async function handle_demo_create(opts) {
11
+ try {
12
+ const adapter = get_hazo_connect_instance();
13
+ const result = await create_demo_user(adapter, opts);
14
+ console.log("");
15
+ console.log("=".repeat(60));
16
+ console.log("DEMO USER CREATED");
17
+ console.log("=".repeat(60));
18
+ console.log("");
19
+ console.log(` Email: ${result.email}`);
20
+ console.log(` Password: ${result.password}`);
21
+ console.log(` User ID: ${result.user_id}`);
22
+ console.log("");
23
+ console.log(" ✓ Email pre-verified — account is login-ready.");
24
+ console.log(" ✓ Assigned to the default system scope.");
25
+ if (opts.admin) {
26
+ console.log(" ✓ Admin account — global-admin role assigned.");
27
+ }
28
+ console.log("=".repeat(60));
29
+ console.log("");
30
+ }
31
+ catch (error) {
32
+ if (error instanceof DemoSeedNotAllowed) {
33
+ console.error("Demo seeding is disabled. Set HAZO_AUTH_ALLOW_DEMO_SEED=true in a non-production env.");
34
+ process.exit(1);
35
+ }
36
+ console.error(error instanceof Error ? error.message : String(error));
37
+ process.exit(1);
38
+ }
39
+ }
40
+ // section: delete_handler
41
+ /**
42
+ * Deletes demo users matching the given email or prefix(es) and prints a summary.
43
+ * Exits with code 1 if demo seeding is not allowed or an error occurs.
44
+ */
45
+ export async function handle_demo_delete(opts) {
46
+ try {
47
+ const adapter = get_hazo_connect_instance();
48
+ const result = await delete_demo_users(adapter, opts);
49
+ console.log(`✓ Deleted ${result.deleted} demo user(s).`);
50
+ }
51
+ catch (error) {
52
+ if (error instanceof DemoSeedNotAllowed) {
53
+ console.error("Demo seeding is disabled. Set HAZO_AUTH_ALLOW_DEMO_SEED=true in a non-production env.");
54
+ process.exit(1);
55
+ }
56
+ console.error(error instanceof Error ? error.message : String(error));
57
+ process.exit(1);
58
+ }
59
+ }
package/dist/cli/index.js CHANGED
@@ -8,6 +8,7 @@ import { handle_init } from "./init.js";
8
8
  import { handle_init_users, show_init_users_help } from "./init_users.js";
9
9
  import { handle_init_db } from "./init_db.js";
10
10
  import { handle_init_permissions, show_init_permissions_help } from "./init_permissions.js";
11
+ import { handle_demo_create, handle_demo_delete } from "./demo.js";
11
12
  // section: constants
12
13
  const VERSION = "1.6.0";
13
14
  const HELP_TEXT = `
@@ -20,6 +21,8 @@ Commands:
20
21
  init-db Create SQLite database with hazo_auth schema (standalone)
21
22
  init-permissions Create default permissions from config (no user required)
22
23
  init-users Initialize permissions, roles, and super user from config
24
+ demo-create [DEV ONLY] Create a login-ready demo user (needs HAZO_AUTH_ALLOW_DEMO_SEED=true)
25
+ demo-delete [DEV ONLY] Delete demo users by email/prefix (needs HAZO_AUTH_ALLOW_DEMO_SEED=true)
23
26
  validate Check your hazo_auth setup and configuration
24
27
  generate-routes Generate API route files and pages in your project
25
28
  schema Print the canonical SQLite schema SQL
@@ -38,6 +41,8 @@ Examples:
38
41
  npx hazo_auth generate-routes --pages
39
42
  npx hazo_auth generate-routes --all --dir=src/app
40
43
  npx hazo_auth schema
44
+ HAZO_AUTH_ALLOW_DEMO_SEED=true npx hazo_auth demo-create --admin
45
+ HAZO_AUTH_ALLOW_DEMO_SEED=true npx hazo_auth demo-delete --all
41
46
 
42
47
  Documentation:
43
48
  https://github.com/your-repo/hazo_auth/blob/main/SETUP_CHECKLIST.md
@@ -200,6 +205,62 @@ Requires: better-sqlite3 (npm install better-sqlite3)
200
205
  await handle_init_users({ email });
201
206
  break;
202
207
  }
208
+ case "demo-create": {
209
+ if (help) {
210
+ console.log(`
211
+ hazo_auth demo-create [DEV ONLY]
212
+
213
+ Create a login-ready demo user (email pre-verified, assigned to the default system scope).
214
+ Refuses to run unless the env is non-production AND HAZO_AUTH_ALLOW_DEMO_SEED=true.
215
+
216
+ Options:
217
+ --email=<addr> Email for the demo user (default: __demo_<timestamp>@hazo.test)
218
+ --password=<pw> Password (default: Demo_Pass123!)
219
+ --name=<name> Display name (default: Demo User)
220
+ --admin Grant the global-admin role instead of a plain member role
221
+ `);
222
+ return;
223
+ }
224
+ const demoCreateOpts = {};
225
+ for (const arg of args) {
226
+ if (arg.startsWith("--email="))
227
+ demoCreateOpts.email = arg.replace("--email=", "");
228
+ else if (arg.startsWith("--password="))
229
+ demoCreateOpts.password = arg.replace("--password=", "");
230
+ else if (arg.startsWith("--name="))
231
+ demoCreateOpts.name = arg.replace("--name=", "");
232
+ else if (arg === "--admin")
233
+ demoCreateOpts.admin = true;
234
+ }
235
+ await handle_demo_create(demoCreateOpts);
236
+ break;
237
+ }
238
+ case "demo-delete": {
239
+ if (help) {
240
+ console.log(`
241
+ hazo_auth demo-delete [DEV ONLY]
242
+
243
+ Delete demo users (and their scope rows) by email or prefix. Defaults to the __demo_ prefix.
244
+ Refuses to run unless the env is non-production AND HAZO_AUTH_ALLOW_DEMO_SEED=true.
245
+
246
+ Options:
247
+ --email=<addr> Delete only the user with this exact email
248
+ --prefix=<p> Delete users whose email starts with this prefix (default: __demo_)
249
+ --all Delete all __demo_-prefixed users (the default behaviour)
250
+ `);
251
+ return;
252
+ }
253
+ const demoDeleteOpts = {};
254
+ for (const arg of args) {
255
+ if (arg.startsWith("--email="))
256
+ demoDeleteOpts.email = arg.replace("--email=", "");
257
+ else if (arg.startsWith("--prefix="))
258
+ demoDeleteOpts.prefixes = [arg.replace("--prefix=", "")];
259
+ }
260
+ // --all is a no-op / explicit default (prefixes stays undefined → service defaults to ['__demo_'])
261
+ await handle_demo_delete(demoDeleteOpts);
262
+ break;
263
+ }
203
264
  case "validate":
204
265
  if (help) {
205
266
  console.log(`
@@ -0,0 +1,43 @@
1
+ import type { HazoConnectAdapter } from "hazo_connect";
2
+ export declare class DemoSeedNotAllowed extends Error {
3
+ constructor(message: string);
4
+ }
5
+ /**
6
+ * Throws DemoSeedNotAllowed if the current environment is production
7
+ * or if HAZO_AUTH_ALLOW_DEMO_SEED is not set to "true".
8
+ * This must be called at the top of every exported function in this module.
9
+ */
10
+ export declare function assert_demo_seed_allowed(): void;
11
+ export type CreateDemoUserOptions = {
12
+ email?: string;
13
+ password?: string;
14
+ name?: string;
15
+ admin?: boolean;
16
+ };
17
+ export type CreateDemoUserResult = {
18
+ user_id: string;
19
+ email: string;
20
+ password: string;
21
+ };
22
+ export type DeleteDemoUsersOptions = {
23
+ email?: string;
24
+ prefixes?: string[];
25
+ };
26
+ /**
27
+ * Creates a demo user in the database, assigning them to the default system scope.
28
+ * Idempotent on email — if the user already exists their id is reused and the
29
+ * scope/role assignment is still ensured.
30
+ *
31
+ * REQUIRES: non-production env + HAZO_AUTH_ALLOW_DEMO_SEED=true
32
+ */
33
+ export declare function create_demo_user(adapter: HazoConnectAdapter, opts?: CreateDemoUserOptions): Promise<CreateDemoUserResult>;
34
+ /**
35
+ * Deletes demo users (and their scope rows) whose email matches the given
36
+ * address or any of the given prefixes. Defaults to the "__demo_" prefix.
37
+ *
38
+ * REQUIRES: non-production env + HAZO_AUTH_ALLOW_DEMO_SEED=true
39
+ */
40
+ export declare function delete_demo_users(adapter: HazoConnectAdapter, opts?: DeleteDemoUsersOptions): Promise<{
41
+ deleted: number;
42
+ }>;
43
+ //# sourceMappingURL=demo_account_service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"demo_account_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/demo_account_service.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AASvD,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AAID;;;;GAIG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAY/C;AAID,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB,CAAC;AAIF;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,GAAE,qBAA0B,GAC/B,OAAO,CAAC,oBAAoB,CAAC,CAwG/B;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,GAAE,sBAA2B,GAChC,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAuC9B"}
@@ -0,0 +1,171 @@
1
+ // file_description: dev-only service for seeding demo accounts in hazo_auth; physically blocked in production
2
+ import { createCrudService } from "hazo_connect/server";
3
+ import argon2 from "argon2";
4
+ import { randomUUID } from "crypto";
5
+ import { DEFAULT_SYSTEM_SCOPE_ID } from "./scope_service.js";
6
+ import { GLOBAL_ADMIN_PERMISSION } from "../constants.js";
7
+ // section: errors
8
+ export class DemoSeedNotAllowed extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "DemoSeedNotAllowed";
12
+ }
13
+ }
14
+ // section: guards
15
+ /**
16
+ * Throws DemoSeedNotAllowed if the current environment is production
17
+ * or if HAZO_AUTH_ALLOW_DEMO_SEED is not set to "true".
18
+ * This must be called at the top of every exported function in this module.
19
+ */
20
+ export function assert_demo_seed_allowed() {
21
+ var _a, _b;
22
+ const env = (_b = (_a = process.env.HAZO_ENV) !== null && _a !== void 0 ? _a : process.env.NODE_ENV) !== null && _b !== void 0 ? _b : "development";
23
+ if (env === "production") {
24
+ throw new DemoSeedNotAllowed("Demo seeding is disabled. Requires a non-production env AND HAZO_AUTH_ALLOW_DEMO_SEED=true.");
25
+ }
26
+ if (process.env.HAZO_AUTH_ALLOW_DEMO_SEED !== "true") {
27
+ throw new DemoSeedNotAllowed("Demo seeding is disabled. Requires a non-production env AND HAZO_AUTH_ALLOW_DEMO_SEED=true.");
28
+ }
29
+ }
30
+ // section: helpers
31
+ /**
32
+ * Creates a demo user in the database, assigning them to the default system scope.
33
+ * Idempotent on email — if the user already exists their id is reused and the
34
+ * scope/role assignment is still ensured.
35
+ *
36
+ * REQUIRES: non-production env + HAZO_AUTH_ALLOW_DEMO_SEED=true
37
+ */
38
+ export async function create_demo_user(adapter, opts = {}) {
39
+ var _a, _b, _c, _d;
40
+ assert_demo_seed_allowed();
41
+ const email = (_a = opts.email) !== null && _a !== void 0 ? _a : `__demo_${Date.now()}@hazo.test`;
42
+ const password = (_b = opts.password) !== null && _b !== void 0 ? _b : "Demo_Pass123!";
43
+ const name = (_c = opts.name) !== null && _c !== void 0 ? _c : "Demo User";
44
+ const admin = (_d = opts.admin) !== null && _d !== void 0 ? _d : false;
45
+ const now = new Date().toISOString();
46
+ const users_service = createCrudService(adapter, "hazo_users");
47
+ const roles_service = createCrudService(adapter, "hazo_roles");
48
+ const permissions_service = createCrudService(adapter, "hazo_permissions");
49
+ const role_permissions_service = createCrudService(adapter, "hazo_role_permissions", { primaryKeys: ["role_id", "permission_id"], autoId: false });
50
+ const user_scopes_service = createCrudService(adapter, "hazo_user_scopes", {
51
+ primaryKeys: ["user_id", "scope_id"],
52
+ autoId: false,
53
+ });
54
+ // Idempotent: reuse existing user if email already registered
55
+ let user_id;
56
+ const existing_users = await users_service.findBy({ email_address: email });
57
+ if (Array.isArray(existing_users) && existing_users.length > 0) {
58
+ user_id = existing_users[0].id;
59
+ }
60
+ else {
61
+ const password_hash = await argon2.hash(password);
62
+ user_id = randomUUID();
63
+ await users_service.insert({
64
+ id: user_id,
65
+ email_address: email,
66
+ password_hash,
67
+ email_verified: true,
68
+ status: "ACTIVE",
69
+ login_attempts: 0,
70
+ auth_providers: "email",
71
+ name,
72
+ created_at: now,
73
+ changed_at: now,
74
+ });
75
+ }
76
+ // Ensure role exists
77
+ const role_name = admin ? "default_super_user_role" : "demo_member_role";
78
+ const existing_roles = await roles_service.findBy({ role_name });
79
+ let role_id;
80
+ if (Array.isArray(existing_roles) && existing_roles.length > 0) {
81
+ role_id = existing_roles[0].id;
82
+ }
83
+ else {
84
+ const new_role = await roles_service.insert({ role_name, created_at: now, changed_at: now });
85
+ role_id = Array.isArray(new_role)
86
+ ? new_role[0].id
87
+ : new_role.id;
88
+ }
89
+ // Admin users: ensure GLOBAL_ADMIN_PERMISSION is in the catalog and linked to the role
90
+ if (admin) {
91
+ // Ensure permission exists
92
+ const existing_perms = await permissions_service.findBy({
93
+ permission_name: GLOBAL_ADMIN_PERMISSION,
94
+ });
95
+ let permission_id;
96
+ if (Array.isArray(existing_perms) && existing_perms.length > 0) {
97
+ permission_id = existing_perms[0].id;
98
+ }
99
+ else {
100
+ const new_perm = await permissions_service.insert({
101
+ permission_name: GLOBAL_ADMIN_PERMISSION,
102
+ description: "Global admin — access to all scopes and operations",
103
+ created_at: now,
104
+ changed_at: now,
105
+ });
106
+ permission_id = Array.isArray(new_perm)
107
+ ? new_perm[0].id
108
+ : new_perm.id;
109
+ }
110
+ // Ensure role→permission link
111
+ const existing_rp = await role_permissions_service.findBy({ role_id, permission_id });
112
+ if (!Array.isArray(existing_rp) || existing_rp.length === 0) {
113
+ await role_permissions_service.insert({ role_id, permission_id });
114
+ }
115
+ }
116
+ // Ensure user→scope assignment
117
+ const existing_user_scopes = await user_scopes_service.findBy({
118
+ user_id,
119
+ scope_id: DEFAULT_SYSTEM_SCOPE_ID,
120
+ });
121
+ if (!Array.isArray(existing_user_scopes) || existing_user_scopes.length === 0) {
122
+ await user_scopes_service.insert({
123
+ user_id,
124
+ scope_id: DEFAULT_SYSTEM_SCOPE_ID,
125
+ root_scope_id: DEFAULT_SYSTEM_SCOPE_ID,
126
+ role_id,
127
+ created_at: now,
128
+ changed_at: now,
129
+ });
130
+ }
131
+ return { user_id, email, password };
132
+ }
133
+ /**
134
+ * Deletes demo users (and their scope rows) whose email matches the given
135
+ * address or any of the given prefixes. Defaults to the "__demo_" prefix.
136
+ *
137
+ * REQUIRES: non-production env + HAZO_AUTH_ALLOW_DEMO_SEED=true
138
+ */
139
+ export async function delete_demo_users(adapter, opts = {}) {
140
+ var _a;
141
+ assert_demo_seed_allowed();
142
+ const prefixes = (_a = opts.prefixes) !== null && _a !== void 0 ? _a : ["__demo_"];
143
+ const users = createCrudService(adapter, "hazo_users");
144
+ const user_scopes_service = createCrudService(adapter, "hazo_user_scopes", {
145
+ primaryKeys: ["user_id", "scope_id"],
146
+ autoId: false,
147
+ });
148
+ const all = await users.list((qb) => qb.limit(500));
149
+ const targets = all.filter((u) => {
150
+ if (opts.email && u.email_address === opts.email)
151
+ return true;
152
+ return prefixes.some((p) => typeof u.email_address === "string" && u.email_address.startsWith(p));
153
+ });
154
+ let deleted = 0;
155
+ for (const u of targets) {
156
+ const uid = u.id;
157
+ try {
158
+ // Best-effort: delete this user's scope rows. hazo_user_scopes has a
159
+ // composite PK (no `id` column); deleteById on a multi-key table falls
160
+ // back to the first primary key (user_id), so this removes all of the
161
+ // user's scope rows in one call.
162
+ await user_scopes_service.deleteById(uid).catch(() => { });
163
+ await users.deleteById(uid);
164
+ deleted += 1;
165
+ }
166
+ catch (_b) {
167
+ // best-effort — skip users that can't be deleted
168
+ }
169
+ }
170
+ return { deleted };
171
+ }
@@ -21,6 +21,24 @@ export type GoogleTokenStatus = {
21
21
  scopes: string;
22
22
  expires_at: string | null;
23
23
  };
24
+ /**
25
+ * Indirection around the hazo_secure/crypto dynamic import.
26
+ *
27
+ * The import specifier MUST be a string literal. hazo_secure is an optional
28
+ * peer dep, so this used to go through hazo_core's `optional_import(pkg)` — but
29
+ * that performs a dynamic `import(variable)`, which Turbopack/webpack cannot
30
+ * statically resolve when consumers bundle their server. The import then fails
31
+ * unconditionally and sign-in dies on GoogleTokenStorageUnconfigured even when
32
+ * hazo_secure is installed. A literal `import("hazo_secure/crypto")` is left as
33
+ * a native external import and resolves at runtime (consumers must list
34
+ * hazo_secure in serverExternalPackages so it isn't bundled).
35
+ *
36
+ * Wrapped in an object so tests can stub `_cryptoLoader.load` without touching
37
+ * the literal specifier. Not part of the public API (underscore-prefixed).
38
+ */
39
+ export declare const _cryptoLoader: {
40
+ load: () => Promise<typeof import("hazo_secure/crypto")>;
41
+ };
24
42
  /**
25
43
  * Stores (inserts or updates) Google OAuth tokens for a user, encrypting sensitive fields.
26
44
  * Throws GoogleTokenStorageUnconfigured if hazo_secure/crypto is unavailable or keys are missing.
@@ -1 +1 @@
1
- {"version":3,"file":"google_token_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/google_token_service.ts"],"names":[],"mappings":"AAUA,qBAAa,8BAA+B,SAAQ,KAAK;;CAOxD;AAID,MAAM,MAAM,2BAA2B,GAAG;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GACzB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAClD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,eAAe,GAAG,oBAAoB,GAAG,gBAAgB,GAAG,cAAc,CAAA;CAAE,CAAC;AAErG,MAAM,MAAM,iBAAiB,GAAG;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AA6CF;;;GAGG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,2BAA2B,GAClC,OAAO,CAAC,IAAI,CAAC,CA0Ef;AAID;;;GAGG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAC3B,OAAO,CAAC,iBAAiB,CAAC,CA6I5B;AAID;;GAEG;AACH,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA6D1C;AAID;;;GAGG;AACH,wBAAsB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAexF"}
1
+ {"version":3,"file":"google_token_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/google_token_service.ts"],"names":[],"mappings":"AASA,qBAAa,8BAA+B,SAAQ,KAAK;;CAOxD;AAID,MAAM,MAAM,2BAA2B,GAAG;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GACzB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAClD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,eAAe,GAAG,oBAAoB,GAAG,gBAAgB,GAAG,cAAc,CAAA;CAAE,CAAC;AAErG,MAAM,MAAM,iBAAiB,GAAG;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAmCF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,aAAa;gBACd,OAAO,CAAC,cAAc,oBAAoB,CAAC,CAAC;CACvD,CAAC;AAeF;;;GAGG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,2BAA2B,GAClC,OAAO,CAAC,IAAI,CAAC,CA0Ef;AAID;;;GAGG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAC3B,OAAO,CAAC,iBAAiB,CAAC,CA6I5B;AAID;;GAEG;AACH,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA6D1C;AAID;;;GAGG;AACH,wBAAsB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAexF"}
@@ -3,7 +3,6 @@
3
3
  import { createCrudService } from "hazo_connect/server";
4
4
  import { get_hazo_connect_instance } from "../hazo_connect_instance.server.js";
5
5
  import { create_app_logger } from "../app_logger.js";
6
- import { optional_import } from "hazo_core";
7
6
  import { randomUUID } from "crypto";
8
7
  // section: errors
9
8
  export class GoogleTokenStorageUnconfigured extends Error {
@@ -26,8 +25,32 @@ function makeKeyProvider(LookupKeyProvider) {
26
25
  return undefined;
27
26
  });
28
27
  }
28
+ /**
29
+ * Indirection around the hazo_secure/crypto dynamic import.
30
+ *
31
+ * The import specifier MUST be a string literal. hazo_secure is an optional
32
+ * peer dep, so this used to go through hazo_core's `optional_import(pkg)` — but
33
+ * that performs a dynamic `import(variable)`, which Turbopack/webpack cannot
34
+ * statically resolve when consumers bundle their server. The import then fails
35
+ * unconditionally and sign-in dies on GoogleTokenStorageUnconfigured even when
36
+ * hazo_secure is installed. A literal `import("hazo_secure/crypto")` is left as
37
+ * a native external import and resolves at runtime (consumers must list
38
+ * hazo_secure in serverExternalPackages so it isn't bundled).
39
+ *
40
+ * Wrapped in an object so tests can stub `_cryptoLoader.load` without touching
41
+ * the literal specifier. Not part of the public API (underscore-prefixed).
42
+ */
43
+ export const _cryptoLoader = {
44
+ load: () => import("hazo_secure/crypto"),
45
+ };
29
46
  async function load_crypto_module() {
30
- const cryptoModule = await optional_import("hazo_secure/crypto");
47
+ let cryptoModule;
48
+ try {
49
+ cryptoModule = await _cryptoLoader.load();
50
+ }
51
+ catch (_a) {
52
+ cryptoModule = null;
53
+ }
31
54
  if (!cryptoModule)
32
55
  throw new GoogleTokenStorageUnconfigured();
33
56
  return cryptoModule;
@@ -19,4 +19,5 @@ export * from "./user_scope_service.js";
19
19
  export * from "./oauth_service.js";
20
20
  export * from "./branding_service.js";
21
21
  export * from "./google_token_service.js";
22
+ export * from "./demo_account_service.js";
22
23
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/services/index.ts"],"names":[],"mappings":"AAEA,cAAc,iBAAiB,CAAC;AAChC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,iBAAiB,CAAC;AAChC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,kCAAkC,CAAC;AACjD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iCAAiC,CAAC;AAChD,cAAc,wBAAwB,CAAC;AACvC,cAAc,iBAAiB,CAAC;AAChC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/services/index.ts"],"names":[],"mappings":"AAEA,cAAc,iBAAiB,CAAC;AAChC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,iBAAiB,CAAC;AAChC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,kCAAkC,CAAC;AACjD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iCAAiC,CAAC;AAChD,cAAc,wBAAwB,CAAC;AACvC,cAAc,iBAAiB,CAAC;AAChC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC"}
@@ -21,3 +21,4 @@ export * from "./user_scope_service.js";
21
21
  export * from "./oauth_service.js";
22
22
  export * from "./branding_service.js";
23
23
  export * from "./google_token_service.js";
24
+ export * from "./demo_account_service.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hazo_auth",
3
- "version": "10.2.0",
3
+ "version": "10.2.2",
4
4
  "description": "Zero-config authentication UI components for Next.js with RBAC, OAuth, scope-based multi-tenancy, and invitations",
5
5
  "keywords": [
6
6
  "authentication",
@@ -206,6 +206,8 @@
206
206
  "dev:server": "tsx src/server/index.ts",
207
207
  "migrate": "tsx scripts/apply_migration.ts",
208
208
  "init-users": "tsx scripts/init_users.ts init_users",
209
+ "demo:create": "tsx --conditions react-server scripts/demo.ts create",
210
+ "demo:delete": "tsx --conditions react-server scripts/demo.ts delete",
209
211
  "test": "cross-env NODE_ENV=test jest --runInBand",
210
212
  "test:watch": "cross-env NODE_ENV=test jest --watch"
211
213
  },
@@ -253,9 +255,9 @@
253
255
  "@radix-ui/react-tabs": "^1.1.0",
254
256
  "@radix-ui/react-tooltip": "^1.2.0",
255
257
  "hazo_api": "^2.4.0",
256
- "hazo_config": "^2.1.10",
257
- "hazo_connect": "^3.6.0",
258
- "hazo_core": "^1.1.0",
258
+ "hazo_config": "^2.1.11",
259
+ "hazo_connect": "^3.7.0",
260
+ "hazo_core": "^1.2.0",
259
261
  "hazo_logs": "^2.0.3",
260
262
  "hazo_notify": "^6.1.3",
261
263
  "hazo_secure": "^1.1.0",
@@ -392,13 +394,13 @@
392
394
  "eslint": "^9.39.1",
393
395
  "eslint-config-next": "^16.0.4",
394
396
  "eslint-plugin-storybook": "^10.0.6",
395
- "hazo_api": "^2.4.0",
396
- "hazo_config": "^2.1.10",
397
- "hazo_connect": "^3.6.0",
398
- "hazo_core": "^1.1.0",
397
+ "hazo_api": "^2.5.0",
398
+ "hazo_config": "^2.1.11",
399
+ "hazo_connect": "^3.7.0",
400
+ "hazo_core": "^1.2.0",
399
401
  "hazo_logs": "^2.0.3",
400
402
  "hazo_notify": "^6.1.3",
401
- "hazo_ui": "^4.0.0",
403
+ "hazo_ui": "^4.2.0",
402
404
  "input-otp": "^1.4.0",
403
405
  "jest": "^30.2.0",
404
406
  "jest-environment-jsdom": "^30.0.0",