@dreamtree-org/landing-kit 0.4.0 → 0.5.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 CHANGED
@@ -179,6 +179,24 @@ export const GET = () => demoAccountsResponse({ accounts: [...] });
179
179
  | `DEMO_ACCOUNTS=1` | turn the endpoint on (off by default; leave it off in production) |
180
180
  | `DEMO_ACCOUNTS_JSON` | `[{ label, identifier, password, company? }]`, replaces the built-in list (accounts a deployment created itself) |
181
181
 
182
+ #### The `default` demo company (every pro app)
183
+
184
+ Every pro app seeds a company with slug **`default`** — its branches and **one user per role** —
185
+ so the whole product can be demoed end to end, and the chips list exactly those users:
186
+
187
+ ```js
188
+ const { demoUsers, demoAccountsExpress, requireDemoPassword } = require('@dreamtree-org/landing-kit/demo-server');
189
+ const manifest = [{ role: 'tenant-admin', label: 'Admin' }, { role: 'viewer', label: 'Viewer' }];
190
+
191
+ // demo:seed (idempotent): create `default`, upsert each user, password = requireDemoPassword()
192
+ // endpoint: one chip per role, emails demoEmail(product, role) → tenant-admin@taskpro.demo.test
193
+ app.get('/api/auth/demo-accounts', demoAccountsExpress({ accounts: (env) => demoUsers(manifest, { product: 'taskpro', env }) }));
194
+ ```
195
+
196
+ | Env | Meaning |
197
+ |---|---|
198
+ | `DEMO_PASSWORD` | the demo users' password (≥ 12 chars); `demo:seed` resets every demo user to it, the chips show it. Unset → no chips |
199
+
182
200
  Company-first logins pick the chip on the company step: call `stashDemoFill(account)` there and
183
201
  `takeDemoFill()` when the password step mounts (sessionStorage, read once). Outside AuthShell, render
184
202
  `<DemoAccounts load onSelect />` behind `useTapReveal()` yourself.
package/demo-server.d.ts CHANGED
@@ -6,7 +6,7 @@ export interface DemoAccountsOptions {
6
6
  /** The product's own gate (e.g. not a production tenant). DEMO_ACCOUNTS=1 is required as well. */
7
7
  enabled?: boolean | undefined;
8
8
  /** Built-in list, or a function of env (read seed passwords from the same vars the seed script uses). */
9
- accounts?: DemoAccount[] | ((env: Env) => DemoAccount[]) | undefined;
9
+ accounts?: DemoAccount[] | ((env: Env) => DemoAccount[] | Promise<DemoAccount[]>) | undefined;
10
10
  env?: Env | undefined;
11
11
  }
12
12
 
@@ -16,9 +16,26 @@ export type DemoAccountsResult =
16
16
 
17
17
  export declare const DEMO_ACCOUNTS_HEADERS: Record<string, string>;
18
18
  export declare function demoAccountsEnabled(opts?: Pick<DemoAccountsOptions, 'enabled' | 'env'>): boolean;
19
- /** 404 unless enabled and DEMO_ACCOUNTS=1; DEMO_ACCOUNTS_JSON replaces `accounts`. */
20
- export declare function resolveDemoAccounts(opts?: DemoAccountsOptions): DemoAccountsResult;
19
+ /** 404 unless enabled and DEMO_ACCOUNTS=1; DEMO_ACCOUNTS_JSON replaces `accounts`. A Promise when `accounts` is async. */
20
+ export declare function resolveDemoAccounts(opts?: DemoAccountsOptions): DemoAccountsResult | Promise<DemoAccountsResult>;
21
21
  /** Express handler for `GET /api/auth/demo-accounts`. */
22
22
  export declare function demoAccountsExpress(opts?: DemoAccountsOptions): (req: any, res: any) => void;
23
23
  /** Fetch-API Response for a Next.js route handler. */
24
- export declare function demoAccountsResponse(opts?: DemoAccountsOptions): Response;
24
+ export declare function demoAccountsResponse(opts?: DemoAccountsOptions): Response | Promise<Response>;
25
+
26
+ /** One row per role in a product's demo manifest. */
27
+ export interface DemoRole {
28
+ role: string;
29
+ label: string;
30
+ /** Defaults to demoEmail(product, role). */
31
+ email?: string | undefined;
32
+ }
33
+ /** Slug of the shared demo company every pro app seeds: 'default'. */
34
+ export declare const DEMO_COMPANY: 'default';
35
+ export declare const DEMO_PASSWORD_MIN: number;
36
+ /** `${role}@${product}.demo.test` (lower-case, dashes). */
37
+ export declare function demoEmail(product: string, role: string): string;
38
+ /** Manifest → chips in the `default` company with env.DEMO_PASSWORD; [] when DEMO_PASSWORD is unset. */
39
+ export declare function demoUsers(manifest: DemoRole[], opts: { product: string; env?: Env | undefined; company?: string | undefined }): DemoAccount[];
40
+ /** Throws unless DEMO_PASSWORD is set and at least DEMO_PASSWORD_MIN characters; returns it. */
41
+ export declare function requireDemoPassword(env?: Env): string;
@@ -20,9 +20,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  var demo_server_exports = {};
21
21
  __export(demo_server_exports, {
22
22
  DEMO_ACCOUNTS_HEADERS: () => DEMO_ACCOUNTS_HEADERS,
23
+ DEMO_COMPANY: () => DEMO_COMPANY,
24
+ DEMO_PASSWORD_MIN: () => DEMO_PASSWORD_MIN,
23
25
  demoAccountsEnabled: () => demoAccountsEnabled,
24
26
  demoAccountsExpress: () => demoAccountsExpress,
25
27
  demoAccountsResponse: () => demoAccountsResponse,
28
+ demoEmail: () => demoEmail,
29
+ demoUsers: () => demoUsers,
30
+ requireDemoPassword: () => requireDemoPassword,
26
31
  resolveDemoAccounts: () => resolveDemoAccounts
27
32
  });
28
33
  module.exports = __toCommonJS(demo_server_exports);
@@ -47,29 +52,70 @@ function clean(accounts) {
47
52
  function demoAccountsEnabled({ enabled = true, env = process.env } = {}) {
48
53
  return Boolean(enabled) && env.DEMO_ACCOUNTS === "1";
49
54
  }
50
- function resolveDemoAccounts({ enabled = true, accounts = [], env = process.env } = {}) {
51
- if (!demoAccountsEnabled({ enabled, env })) return { status: 404, body: { error: "Not found" } };
52
- const list = clean(fromEnvJson(env.DEMO_ACCOUNTS_JSON) ?? (typeof accounts === "function" ? accounts(env) : accounts));
55
+ function result(accounts) {
56
+ if (!accounts) return { status: 404, body: { error: "Not found" } };
57
+ const list = clean(accounts);
53
58
  if (!list.length) return { status: 404, body: { error: "Not found" } };
54
59
  return { status: 200, body: { accounts: list } };
55
60
  }
61
+ function resolveDemoAccounts({ enabled = true, accounts = [], env = process.env } = {}) {
62
+ const on = demoAccountsEnabled({ enabled, env });
63
+ if (!on) return result(null);
64
+ const override = fromEnvJson(env.DEMO_ACCOUNTS_JSON);
65
+ if (override) return result(override);
66
+ const list = typeof accounts === "function" ? accounts(env) : accounts;
67
+ return list && typeof list.then === "function" ? list.then((l) => result(l || []), () => result(null)) : result(list || []);
68
+ }
56
69
  var DEMO_ACCOUNTS_HEADERS = { "Cache-Control": "no-store", "X-Robots-Tag": "noindex" };
57
70
  function demoAccountsExpress(opts = {}) {
58
- return (req, res) => {
59
- const { status, body } = resolveDemoAccounts(opts);
71
+ const send = (res, { status, body }) => {
60
72
  res.set(DEMO_ACCOUNTS_HEADERS);
61
73
  res.status(status).json(body);
62
74
  };
75
+ return (req, res) => {
76
+ const r = resolveDemoAccounts(opts);
77
+ if (typeof r.then === "function") return r.then((x) => send(res, x));
78
+ send(res, r);
79
+ };
63
80
  }
64
81
  function demoAccountsResponse(opts = {}) {
65
- const { status, body } = resolveDemoAccounts(opts);
66
- return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json", ...DEMO_ACCOUNTS_HEADERS } });
82
+ const toResponse = ({ status, body }) => new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json", ...DEMO_ACCOUNTS_HEADERS } });
83
+ const r = resolveDemoAccounts(opts);
84
+ return typeof r.then === "function" ? r.then(toResponse) : toResponse(r);
85
+ }
86
+ var DEMO_COMPANY = "default";
87
+ var DEMO_PASSWORD_MIN = 12;
88
+ function demoEmail(product, role) {
89
+ const slug = (s) => String(s).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
90
+ return `${slug(role)}@${slug(product)}.demo.test`;
91
+ }
92
+ function demoUsers(manifest, { product, env = process.env, company = DEMO_COMPANY } = {}) {
93
+ const password = env.DEMO_PASSWORD;
94
+ if (!password) return [];
95
+ return (manifest || []).map((m) => ({
96
+ label: m.label,
97
+ identifier: m.email || demoEmail(product, m.role),
98
+ password,
99
+ company,
100
+ role: m.role
101
+ }));
102
+ }
103
+ function requireDemoPassword(env = process.env) {
104
+ const p = env.DEMO_PASSWORD;
105
+ if (!p) throw new Error("DEMO_PASSWORD is not set: demo:seed gives every demo user this password (the login chips show it).");
106
+ if (p.length < DEMO_PASSWORD_MIN) throw new Error(`DEMO_PASSWORD must be at least ${DEMO_PASSWORD_MIN} characters.`);
107
+ return p;
67
108
  }
68
109
  // Annotate the CommonJS export names for ESM import in node:
69
110
  0 && (module.exports = {
70
111
  DEMO_ACCOUNTS_HEADERS,
112
+ DEMO_COMPANY,
113
+ DEMO_PASSWORD_MIN,
71
114
  demoAccountsEnabled,
72
115
  demoAccountsExpress,
73
116
  demoAccountsResponse,
117
+ demoEmail,
118
+ demoUsers,
119
+ requireDemoPassword,
74
120
  resolveDemoAccounts
75
121
  });
@@ -20,28 +20,69 @@ function clean(accounts) {
20
20
  function demoAccountsEnabled({ enabled = true, env = process.env } = {}) {
21
21
  return Boolean(enabled) && env.DEMO_ACCOUNTS === "1";
22
22
  }
23
- function resolveDemoAccounts({ enabled = true, accounts = [], env = process.env } = {}) {
24
- if (!demoAccountsEnabled({ enabled, env })) return { status: 404, body: { error: "Not found" } };
25
- const list = clean(fromEnvJson(env.DEMO_ACCOUNTS_JSON) ?? (typeof accounts === "function" ? accounts(env) : accounts));
23
+ function result(accounts) {
24
+ if (!accounts) return { status: 404, body: { error: "Not found" } };
25
+ const list = clean(accounts);
26
26
  if (!list.length) return { status: 404, body: { error: "Not found" } };
27
27
  return { status: 200, body: { accounts: list } };
28
28
  }
29
+ function resolveDemoAccounts({ enabled = true, accounts = [], env = process.env } = {}) {
30
+ const on = demoAccountsEnabled({ enabled, env });
31
+ if (!on) return result(null);
32
+ const override = fromEnvJson(env.DEMO_ACCOUNTS_JSON);
33
+ if (override) return result(override);
34
+ const list = typeof accounts === "function" ? accounts(env) : accounts;
35
+ return list && typeof list.then === "function" ? list.then((l) => result(l || []), () => result(null)) : result(list || []);
36
+ }
29
37
  var DEMO_ACCOUNTS_HEADERS = { "Cache-Control": "no-store", "X-Robots-Tag": "noindex" };
30
38
  function demoAccountsExpress(opts = {}) {
31
- return (req, res) => {
32
- const { status, body } = resolveDemoAccounts(opts);
39
+ const send = (res, { status, body }) => {
33
40
  res.set(DEMO_ACCOUNTS_HEADERS);
34
41
  res.status(status).json(body);
35
42
  };
43
+ return (req, res) => {
44
+ const r = resolveDemoAccounts(opts);
45
+ if (typeof r.then === "function") return r.then((x) => send(res, x));
46
+ send(res, r);
47
+ };
36
48
  }
37
49
  function demoAccountsResponse(opts = {}) {
38
- const { status, body } = resolveDemoAccounts(opts);
39
- return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json", ...DEMO_ACCOUNTS_HEADERS } });
50
+ const toResponse = ({ status, body }) => new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json", ...DEMO_ACCOUNTS_HEADERS } });
51
+ const r = resolveDemoAccounts(opts);
52
+ return typeof r.then === "function" ? r.then(toResponse) : toResponse(r);
53
+ }
54
+ var DEMO_COMPANY = "default";
55
+ var DEMO_PASSWORD_MIN = 12;
56
+ function demoEmail(product, role) {
57
+ const slug = (s) => String(s).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
58
+ return `${slug(role)}@${slug(product)}.demo.test`;
59
+ }
60
+ function demoUsers(manifest, { product, env = process.env, company = DEMO_COMPANY } = {}) {
61
+ const password = env.DEMO_PASSWORD;
62
+ if (!password) return [];
63
+ return (manifest || []).map((m) => ({
64
+ label: m.label,
65
+ identifier: m.email || demoEmail(product, m.role),
66
+ password,
67
+ company,
68
+ role: m.role
69
+ }));
70
+ }
71
+ function requireDemoPassword(env = process.env) {
72
+ const p = env.DEMO_PASSWORD;
73
+ if (!p) throw new Error("DEMO_PASSWORD is not set: demo:seed gives every demo user this password (the login chips show it).");
74
+ if (p.length < DEMO_PASSWORD_MIN) throw new Error(`DEMO_PASSWORD must be at least ${DEMO_PASSWORD_MIN} characters.`);
75
+ return p;
40
76
  }
41
77
  export {
42
78
  DEMO_ACCOUNTS_HEADERS,
79
+ DEMO_COMPANY,
80
+ DEMO_PASSWORD_MIN,
43
81
  demoAccountsEnabled,
44
82
  demoAccountsExpress,
45
83
  demoAccountsResponse,
84
+ demoEmail,
85
+ demoUsers,
86
+ requireDemoPassword,
46
87
  resolveDemoAccounts
47
88
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreamtree-org/landing-kit",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "The shared marketing site and auth frame (AuthShell + brands) for every Dreamtree product: content-driven landing pages, login/signup shell, Express routes, prerender and SEO files.",
5
5
  "type": "module",
6
6
  "license": "MIT",