@dreamtree-org/landing-kit 0.9.0 → 0.10.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/MIGRATION.md +16 -0
- package/PLATFORM-API.md +17 -13
- package/README.md +41 -1
- package/ai-skills/landing-kit.md +7 -6
- package/auth.css +57 -0
- package/auth.d.ts +2 -2
- package/dist/auth.cjs +143 -38
- package/dist/auth.js +139 -34
- package/dist/index.cjs +605 -540
- package/dist/index.js +573 -508
- package/dist/platform.cjs +1208 -408
- package/dist/platform.js +1209 -409
- package/index.d.ts +25 -0
- package/package.json +3 -2
- package/platform.d.ts +37 -3
- package/styles.css +57 -0
package/MIGRATION.md
CHANGED
|
@@ -173,3 +173,19 @@ version-2 document (`validateStore` from `/schema`). Nothing changes for apps th
|
|
|
173
173
|
Required slots cannot be hidden or removed.
|
|
174
174
|
- `safeHref` now rejects links containing a backslash, and registry lookups (`blockDef`, `templatePage`) ignore
|
|
175
175
|
inherited keys, so `__proto__` or `constructor` is an unknown type.
|
|
176
|
+
|
|
177
|
+
## 0.10.0 — Platform console: plans, settings, support access, domains (additive)
|
|
178
|
+
|
|
179
|
+
The console now shows the v1.1 features that `@dreamtree-org/tenancy` 0.5.0 serves. Nothing appears until the
|
|
180
|
+
server advertises it in `GET /me`, so a product on tenancy 0.4.0 sees no change.
|
|
181
|
+
|
|
182
|
+
- **New pages:** Plans (`/platform/plans`, `plans.read`) and Support access (`/platform/support-access`,
|
|
183
|
+
`support.request`).
|
|
184
|
+
- **New company tabs**, between Admins and the product's `companyTabs`: Subscription, Domains, Settings and
|
|
185
|
+
Support. Each needs its feature and read permission.
|
|
186
|
+
- `'domains'` joins `PLATFORM_FEATURES` and the `PlatformFeature` type. `usePlatform()` gains `features`.
|
|
187
|
+
- New exports: `formatPrice`, `PlansView`, `SupportAccessView`, `SupportGrants`, `SubscriptionTab`,
|
|
188
|
+
`DomainsTab`, `SettingsTab`, `SupportTab`.
|
|
189
|
+
- A product that already has its own settings or subscription tab in `companyTabs` should drop it once the
|
|
190
|
+
server serves the built-in one, or the company page shows both.
|
|
191
|
+
|
package/PLATFORM-API.md
CHANGED
|
@@ -5,7 +5,7 @@ server only through this contract. The server side is `createPlatformApi` in `@d
|
|
|
5
5
|
mounted with `./next` (`app/api/platform/[...path]/route.ts`) or `./express` (`app.use('/api/platform', …)`).
|
|
6
6
|
A product that implements these endpoints itself gets the same console.
|
|
7
7
|
|
|
8
|
-
**Server status.** `@dreamtree-org/tenancy` 0.
|
|
8
|
+
**Server status.** `@dreamtree-org/tenancy` 0.5.0 implements the whole contract: everything marked shipped, and every v1.1 item (TOTP, throttle, rotation, `/me`, roles, audit, plans, settings, support access and domains). The console in landing-kit 0.10.0 shows all of them.
|
|
9
9
|
|
|
10
10
|
The contract was taken from procurementpro's `app/api/platform/**`, the reference console. Paths are
|
|
11
11
|
relative to the base path, `/api/platform` by default (`createPlatformClient({ basePath })`).
|
|
@@ -63,14 +63,16 @@ relative to the base path, `/api/platform` by default (`createPlatformClient({ b
|
|
|
63
63
|
```json
|
|
64
64
|
{
|
|
65
65
|
"admin": { "id": "…", "email": "…", "roles": ["superadmin"], "totpEnrolled": true },
|
|
66
|
-
"permissions": ["companies.read", "companies.write", "admins.write", "jobs.write", "db.reveal", "plans.write", "settings.write", "support.grant"],
|
|
66
|
+
"permissions": ["companies.read", "companies.write", "admins.write", "jobs.write", "db.reveal", "plans.write", "settings.write", "support.grant", "domains.write"],
|
|
67
67
|
"access": { "console": true, "stepUpFresh": false },
|
|
68
68
|
"features": { "jobs": true, "plans": true, "settings": true, "support": true, "domains": true, "audit": true },
|
|
69
69
|
"app": { "key": "procurementpro", "name": "ProcurementPro", "companyLoginPath": "/c/:slug/login" }
|
|
70
70
|
}
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
**Roles.** `superadmin` holds every permission. `support` holds `companies.read`, `admins.read`, `jobs.read`, `audit.read`, `plans.read` and `support.request`.
|
|
73
|
+
**Roles.** `superadmin` holds every permission. `support` holds `companies.read`, `admins.read`, `jobs.read`, `audit.read`, `plans.read`, `domains.read` and `support.request`.
|
|
74
|
+
|
|
75
|
+
**Permissions per v1.1 feature:** plans and subscriptions use `plans.read` / `plans.write`; settings use `settings.read` / `settings.write`; support access uses `support.request` (list and request) and `support.grant` (approve and revoke); domains use `domains.read` / `domains.write`.
|
|
74
76
|
|
|
75
77
|
Until `/me` exists, the console falls back to the shipped feature set (overview, companies, admins, and
|
|
76
78
|
jobs when `GET /jobs` answers `enabled: true`).
|
|
@@ -157,14 +159,16 @@ advertised.
|
|
|
157
159
|
| POST | `/plans` | `Plan` without `id` | **step-up** | v1.1 |
|
|
158
160
|
| PATCH | `/plans/:id` | any `Plan` field | Archive with `{ status: "archived" }`; plans are never deleted. **step-up** | v1.1 |
|
|
159
161
|
| GET | `/companies/:id/subscription` | — | `{ subscription: Subscription \| null, history: Subscription[] }` | v1.1 |
|
|
160
|
-
| PUT | `/companies/:id/subscription` | `{ planId, status, startsAt?, endsAt?, seats?, notes? }` | **step-up** | v1.1 |
|
|
162
|
+
| PUT | `/companies/:id/subscription` | `{ planId, status, startsAt?, endsAt?, seats?, notes? }` | `{ subscription, history }`. Appends a row; an archived plan is refused unless the company is already on it. **step-up** | v1.1 |
|
|
161
163
|
|
|
162
|
-
###
|
|
164
|
+
### Company settings
|
|
163
165
|
|
|
164
166
|
| Method | Path | Body | Response / notes | Status |
|
|
165
167
|
|---|---|---|---|---|
|
|
166
|
-
| GET | `/settings` | `?company=` (
|
|
167
|
-
| PUT | `/settings/:family` | `{ companyId
|
|
168
|
+
| GET | `/settings` | `?company=` (required) | `{ families: SettingsFamily[], version }`. Secret fields carry `set`, never a value | v1.1 |
|
|
169
|
+
| PUT | `/settings/:family` | `{ companyId, values, version }` | `{ ok, changed, families, version }`. A blank secret keeps the stored one; a stale `version` answers 409 `CONFLICT`. The audit records the keys changed, never the values. **step-up** | v1.1 |
|
|
170
|
+
|
|
171
|
+
Settings are per company. Platform-wide settings are not part of the contract yet.
|
|
168
172
|
|
|
169
173
|
### Support access
|
|
170
174
|
|
|
@@ -174,8 +178,8 @@ data (knowledge doc §5.8).
|
|
|
174
178
|
| Method | Path | Body | Response / notes | Status |
|
|
175
179
|
|---|---|---|---|---|
|
|
176
180
|
| GET | `/support-access` | `?company&status` | `{ grants: SupportGrant[] }` | v1.1 |
|
|
177
|
-
| POST | `/support-access` | `{ companyId, reason, minutes }` |
|
|
178
|
-
| POST | `/support-access/:id` | `{ action: "approve" \| "revoke" }` | **step-up** | v1.1 |
|
|
181
|
+
| POST | `/support-access` | `{ companyId, reason, minutes }` | `{ grant }`. Reason 8–255 characters; minutes 15–480. An unapproved request lapses after a day | v1.1 |
|
|
182
|
+
| POST | `/support-access/:id` | `{ action: "approve" \| "revoke" }` | `{ grant }`. The requester cannot approve their own grant; the window starts at approval. **step-up** | v1.1 |
|
|
179
183
|
|
|
180
184
|
### Domains
|
|
181
185
|
|
|
@@ -183,7 +187,7 @@ data (knowledge doc §5.8).
|
|
|
183
187
|
|---|---|---|---|---|
|
|
184
188
|
| GET | `/companies/:id/domains` | — | `{ domains: CompanyDomain[] }` | v1.1 |
|
|
185
189
|
| POST | `/companies/:id/domains` | `{ host }` | `{ domain, verification: { type: "TXT", name, value } }` | v1.1 |
|
|
186
|
-
| POST | `/companies/:id/domains/:domainId` | `{ action: "verify" \| "make-primary" \| "remove" }` | `remove` needs **step-up** | v1.1 |
|
|
190
|
+
| POST | `/companies/:id/domains/:domainId` | `{ action: "verify" \| "make-primary" \| "remove" }` | `verify` answers `{ ok, verified, domain }` (`verified: false` until the TXT record is published). The first verified domain becomes primary. `remove` needs **step-up** | v1.1 |
|
|
187
191
|
|
|
188
192
|
## Shapes
|
|
189
193
|
|
|
@@ -206,8 +210,8 @@ The server writes one `platform_audit_events` row per mutating call. Action name
|
|
|
206
210
|
- `company.admin.create`, `branch.create`
|
|
207
211
|
- `database.test`, `database.replace`, `database.credentials.reveal`
|
|
208
212
|
- `job.run`, `job.retry`, `schedule.create`
|
|
209
|
-
- `plan.update`, `subscription.update`, `settings.update`
|
|
210
|
-
- `support.grant.request`, `support.grant.approve`
|
|
211
|
-
- `domain.add`
|
|
213
|
+
- `plan.create`, `plan.update`, `subscription.update`, `settings.update`
|
|
214
|
+
- `support.grant.request`, `support.grant.approve`, `support.grant.revoke`
|
|
215
|
+
- `domain.add`, `domain.verify`, `domain.primary`, `domain.remove`
|
|
212
216
|
|
|
213
217
|
Products may add their own actions under their app key.
|
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ Every star ⭐, share, and contribution helps more than you know. Thank you for
|
|
|
54
54
|
- `<LandingPage brand={brand} content={content} />` — a product's landing page is a content file.
|
|
55
55
|
- `<AuthShell brand={brand} title="Welcome back">…</AuthShell>` (also available at `@dreamtree-org/landing-kit/auth` + `@dreamtree-org/landing-kit/auth.css`).
|
|
56
56
|
- The built-in `brands` / `getBrand(id)` registry is **deprecated** (warns once, removed in 0.5.0).
|
|
57
|
-
- **Platform admin console** (`@dreamtree-org/landing-kit/platform`, 0.8.0+): one HTTP contract ([PLATFORM-API.md](./PLATFORM-API.md)) that every product's super-administrator console speaks, and its browser client (`createPlatformClient`, `createStepUp`). The server half is `createPlatformApi` in `@dreamtree-org/tenancy`. The shared React shell and views (overview, companies, jobs, admins, audit
|
|
57
|
+
- **Platform admin console** (`@dreamtree-org/landing-kit/platform`, 0.8.0+): one HTTP contract ([PLATFORM-API.md](./PLATFORM-API.md)) that every product's super-administrator console speaks, and its browser client (`createPlatformClient`, `createStepUp`). The server half is `createPlatformApi` in `@dreamtree-org/tenancy`. The shared React shell and views (overview, companies, jobs, admins, audit; and from 0.10.0 plans, subscriptions, company settings, support access and domains) build on this client.
|
|
58
58
|
- Plain CSS (`--dt-*` custom properties). It looks the same under Tailwind 3, Tailwind 4 (preflight or not) or no Tailwind. Dark mode via `[data-theme="dark"]` or `.dark`.
|
|
59
59
|
- Built for prerendered, **un-hydrated** pages (fast and crawlable): `enhanceLanding()` wires the menu and the demo button in a few hundred bytes. It also works hydrated in an SPA or Next.js (`'use client'`, `onBookDemo`, `linkComponent`).
|
|
60
60
|
- Responsive from 320px up. The nav folds into a menu below 1024px. Checked with Playwright at 320/375/768/1024/1440, light and dark.
|
|
@@ -261,6 +261,7 @@ export default function Login() {
|
|
|
261
261
|
| `legalLinks` | `{label, href}[]` | Privacy, Terms, Data Policy | `[]` hides them |
|
|
262
262
|
| `linkComponent` | component | `<a>` | receives both `to` and `href` |
|
|
263
263
|
| `demoAccounts` | `{ load, onSelect, taps? }` | | hidden demo-accounts panel (below) |
|
|
264
|
+
| `ssoProvider` | `{ startUrl, mode, label?, companySlug?, returnTo?, autoRedirect? }` | | "Continue with Dreamtree" (Dreamtree ID single sign-on, below) |
|
|
264
265
|
|
|
265
266
|
#### Hidden demo accounts (every product, same behaviour)
|
|
266
267
|
|
|
@@ -321,6 +322,45 @@ Company-first logins pick the chip on the company step: call `stashDemoFill(acco
|
|
|
321
322
|
`takeDemoFill()` when the password step mounts (sessionStorage, read once). Outside AuthShell, render
|
|
322
323
|
`<DemoAccounts load onSelect />` behind `useTapReveal()` yourself.
|
|
323
324
|
|
|
325
|
+
#### Dreamtree ID single sign-on (`ssoProvider`)
|
|
326
|
+
|
|
327
|
+
Each app maps its `AUTH_MODE` env to `ssoProvider`: `local` → leave it out (the card is byte-for-byte what it
|
|
328
|
+
was), `hybrid` → a **Continue with Dreamtree** button and an "or" divider above your form, `sso` → no form, a
|
|
329
|
+
"Redirecting to Dreamtree ID…" status (`role="status"`) and `window.location.assign` after mount, with the
|
|
330
|
+
same button kept as the fallback if JS or the redirect fails.
|
|
331
|
+
|
|
332
|
+
```jsx
|
|
333
|
+
// hybrid: the local form stays, Dreamtree goes on top
|
|
334
|
+
<AuthShell
|
|
335
|
+
brand={brand}
|
|
336
|
+
title="Sign in"
|
|
337
|
+
ssoProvider={{ startUrl: '/auth/sso/start', mode: 'hybrid', companySlug: slug, returnTo: '/dashboard' }}
|
|
338
|
+
>
|
|
339
|
+
<LoginForm />
|
|
340
|
+
</AuthShell>
|
|
341
|
+
|
|
342
|
+
// sso: Dreamtree only; children (and the demo-accounts panel) are not rendered
|
|
343
|
+
<AuthShell
|
|
344
|
+
brand={brand}
|
|
345
|
+
title="Sign in"
|
|
346
|
+
ssoProvider={{ startUrl: '/auth/sso/start', mode: 'sso', companySlug: params.slug, returnTo: searchParams.get('next') ?? '/' }}
|
|
347
|
+
/>
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
| Field | Type | Default | Meaning |
|
|
351
|
+
|---|---|---|---|
|
|
352
|
+
| `startUrl` | `string` | required | the app's SSO entry point (`/auth/sso/start`); may already have a query or `#hash` |
|
|
353
|
+
| `mode` | `'hybrid' \| 'sso'` | required | see above |
|
|
354
|
+
| `label` | `string` | `"Continue with Dreamtree"` | button text (its accessible name) |
|
|
355
|
+
| `companySlug` | `string` | | sent as `dt_company` (the `/c/[slug]/login` company) |
|
|
356
|
+
| `returnTo` | `string` | | sent as `return_to`; leave it out and the app's start route picks its default |
|
|
357
|
+
| `autoRedirect` | `boolean` | `true` in `sso` | `false` shows only the button (no status, no redirect); ignored in `hybrid` |
|
|
358
|
+
|
|
359
|
+
The link is `buildSsoUrl(startUrl, { returnTo, companySlug })` (also exported): both values are URI-encoded,
|
|
360
|
+
replace same-named params already on `startUrl`, and every other param and the `#hash` are kept. It is pure
|
|
361
|
+
string work, so server rendering never touches `window`; the redirect happens only in an effect, once per
|
|
362
|
+
mount. The spinner does not animate under `prefers-reduced-motion`.
|
|
363
|
+
|
|
324
364
|
### `@dreamtree-org/landing-kit/enhance`
|
|
325
365
|
|
|
326
366
|
`enhanceLanding(root = document, { openDemo | loadDemo, warmDemo, hash = '#book-demo' })`: wires
|
package/ai-skills/landing-kit.md
CHANGED
|
@@ -59,7 +59,7 @@ const { password } = await sensitive(() => api.post(`admins/${id}`, { action: 'r
|
|
|
59
59
|
- **Cancelled step-up.** A step-up the admin cancels rejects with `code: 'CANCELLED'`. Treat it as a no-op, not an error toast.
|
|
60
60
|
- **Enrollment gate.** A 403 `TOTP_ENROLLMENT_REQUIRED`, or `me.access.console === false`, means show the authenticator enrolment screen (`POST me/totp` with `begin`, then `verify`), not an error.
|
|
61
61
|
- **One-time secrets.** New passwords and revealed DB URLs come back exactly once. Show them in a copy-once panel, and never log them or put them in state that persists.
|
|
62
|
-
- **Feature gating.** Call `GET me` (v1.1 servers) and pass the result to `enabledFeatures(PLATFORM_FEATURES, me)`. That decides which nav items to show: jobs, plans, audit, support, settings. Older servers fall back to overview, companies, jobs and admins.
|
|
62
|
+
- **Feature gating.** Call `GET me` (v1.1 servers) and pass the result to `enabledFeatures(PLATFORM_FEATURES, me)`. That decides which nav items and company tabs to show: jobs, plans, audit, support, settings, domains. Older servers fall back to overview, companies, jobs and admins.
|
|
63
63
|
|
|
64
64
|
## React console
|
|
65
65
|
|
|
@@ -76,13 +76,14 @@ const { password } = await sensitive(() => api.post(`admins/${id}`, { action: 'r
|
|
|
76
76
|
- Companies: list, 4-step new-company wizard, detail with overview, branches, database (test, sync, replace, reveal), admins, jobs and activity
|
|
77
77
|
- Background jobs: runs, schedules, workers and queues
|
|
78
78
|
- Platform admins
|
|
79
|
-
- **v1.1
|
|
79
|
+
- **v1.1** (served by tenancy 0.5.0, shown by landing-kit 0.10.0):
|
|
80
80
|
- TOTP sign-in and enrolment, login throttle, `GET /me` with a role-based permission list
|
|
81
81
|
- Global audit viewer and CSV export
|
|
82
|
-
- Plans and
|
|
83
|
-
-
|
|
84
|
-
- Support
|
|
85
|
-
-
|
|
82
|
+
- Plans page, and a Subscription tab on each company
|
|
83
|
+
- Settings tab on each company (secret fields are write-only; saves carry `version`)
|
|
84
|
+
- Support access page, and a Support tab on each company (a second admin approves)
|
|
85
|
+
- Domains tab on each company (TXT verification, primary, remove)
|
|
86
|
+
- **Built in, not product tabs.** Subscription, Domains, Settings and Support are console built-ins that appear when `GET /me` advertises the feature. Don't re-implement them in `companyTabs`.
|
|
86
87
|
|
|
87
88
|
## Security invariants (do not weaken)
|
|
88
89
|
|
package/auth.css
CHANGED
|
@@ -209,6 +209,63 @@
|
|
|
209
209
|
.dt-auth-demo-note { margin: 0.625rem 0 0; font-size: 0.75rem; color: var(--dt-auth-muted); }
|
|
210
210
|
.dt-auth-demo-note code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.75rem; color: inherit; }
|
|
211
211
|
|
|
212
|
+
/* ---- Dreamtree ID single sign-on (ssoProvider, DTID-32) ---- */
|
|
213
|
+
.dt-auth-sso-btn {
|
|
214
|
+
display: flex;
|
|
215
|
+
align-items: center;
|
|
216
|
+
justify-content: center;
|
|
217
|
+
gap: 0.5rem;
|
|
218
|
+
width: 100%;
|
|
219
|
+
min-height: 2.75rem;
|
|
220
|
+
padding: 0.625rem 1rem;
|
|
221
|
+
border: 1px solid var(--dt-brand);
|
|
222
|
+
border-radius: 0.5rem;
|
|
223
|
+
background: var(--dt-brand);
|
|
224
|
+
color: #fff;
|
|
225
|
+
font: inherit;
|
|
226
|
+
font-size: 0.9375rem;
|
|
227
|
+
font-weight: 600;
|
|
228
|
+
text-decoration: none;
|
|
229
|
+
transition: background-color 120ms;
|
|
230
|
+
}
|
|
231
|
+
.dt-auth-sso-btn:hover { background: color-mix(in oklab, var(--dt-brand) 88%, black); }
|
|
232
|
+
.dt-auth-sso-btn:focus-visible { outline: 2px solid var(--dt-brand); outline-offset: 2px; }
|
|
233
|
+
.dt-auth-sso-icon { flex: none; }
|
|
234
|
+
.dt-auth-or {
|
|
235
|
+
display: flex;
|
|
236
|
+
align-items: center;
|
|
237
|
+
gap: 0.75rem;
|
|
238
|
+
margin: 1.25rem 0;
|
|
239
|
+
font-size: 0.75rem;
|
|
240
|
+
letter-spacing: 0.08em;
|
|
241
|
+
text-transform: uppercase;
|
|
242
|
+
color: var(--dt-auth-muted);
|
|
243
|
+
}
|
|
244
|
+
.dt-auth-or::before,
|
|
245
|
+
.dt-auth-or::after { content: ''; flex: 1; border-top: 1px solid var(--dt-auth-border); }
|
|
246
|
+
.dt-auth-sso-status {
|
|
247
|
+
display: flex;
|
|
248
|
+
align-items: center;
|
|
249
|
+
gap: 0.625rem;
|
|
250
|
+
margin: 0 0 1rem;
|
|
251
|
+
font-size: 0.875rem;
|
|
252
|
+
color: var(--dt-auth-muted);
|
|
253
|
+
}
|
|
254
|
+
.dt-auth-sso-spinner {
|
|
255
|
+
flex: none;
|
|
256
|
+
width: 1rem;
|
|
257
|
+
height: 1rem;
|
|
258
|
+
border: 2px solid var(--dt-brand-soft);
|
|
259
|
+
border-top-color: var(--dt-brand);
|
|
260
|
+
border-radius: 999px;
|
|
261
|
+
animation: dt-auth-sso-spin 0.8s linear infinite;
|
|
262
|
+
}
|
|
263
|
+
@keyframes dt-auth-sso-spin { to { transform: rotate(360deg); } }
|
|
264
|
+
@media (prefers-reduced-motion: reduce) {
|
|
265
|
+
.dt-auth-sso-spinner { animation: none; }
|
|
266
|
+
.dt-auth-sso-btn { transition: none; }
|
|
267
|
+
}
|
|
268
|
+
|
|
212
269
|
/* ---- phones ---- */
|
|
213
270
|
.dt-auth-column,
|
|
214
271
|
.dt-auth-card { min-width: 0; }
|
package/auth.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export type { BrandId, Brand, LegalLink, AuthShellProps, AuthDemoAccounts, DemoAccount, DemoAccountsResponse, DemoAccountsProps } from './index.js';
|
|
2
|
-
export { AuthShell, brandStyle, brands, getBrand, toLandingBrand, DemoAccounts, useTapReveal, DEMO_ACCOUNTS_URL, fetchDemoAccounts, normalizeDemoAccounts, stashDemoFill, takeDemoFill } from './index.js';
|
|
1
|
+
export type { BrandId, Brand, LegalLink, AuthShellProps, SsoProvider, AuthDemoAccounts, DemoAccount, DemoAccountsResponse, DemoAccountsProps } from './index.js';
|
|
2
|
+
export { AuthShell, buildSsoUrl, brandStyle, brands, getBrand, toLandingBrand, DemoAccounts, useTapReveal, DEMO_ACCOUNTS_URL, fetchDemoAccounts, normalizeDemoAccounts, stashDemoFill, takeDemoFill } from './index.js';
|
|
3
3
|
export type { BrandFile, BrandCopy } from './index.js';
|
package/dist/auth.cjs
CHANGED
|
@@ -25,6 +25,7 @@ __export(auth_exports, {
|
|
|
25
25
|
DemoAccounts: () => DemoAccounts,
|
|
26
26
|
brandStyle: () => brandStyle,
|
|
27
27
|
brands: () => brands,
|
|
28
|
+
buildSsoUrl: () => buildSsoUrl,
|
|
28
29
|
fetchDemoAccounts: () => fetchDemoAccounts,
|
|
29
30
|
getBrand: () => getBrand,
|
|
30
31
|
normalizeDemoAccounts: () => normalizeDemoAccounts,
|
|
@@ -36,7 +37,7 @@ __export(auth_exports, {
|
|
|
36
37
|
module.exports = __toCommonJS(auth_exports);
|
|
37
38
|
|
|
38
39
|
// src/AuthShell.jsx
|
|
39
|
-
var
|
|
40
|
+
var import_react5 = require("react");
|
|
40
41
|
|
|
41
42
|
// src/DemoAccounts.jsx
|
|
42
43
|
var import_react = require("react");
|
|
@@ -122,13 +123,116 @@ function DemoAccounts({ load, onSelect, className = "" }) {
|
|
|
122
123
|
] });
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
// src/
|
|
126
|
+
// src/DreamtreeSso.jsx
|
|
127
|
+
var import_react3 = require("react");
|
|
128
|
+
|
|
129
|
+
// src/sso.js
|
|
130
|
+
function decodeKey(part) {
|
|
131
|
+
const key = part.split("=")[0];
|
|
132
|
+
try {
|
|
133
|
+
return decodeURIComponent(key.replace(/\+/g, " "));
|
|
134
|
+
} catch {
|
|
135
|
+
return key;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function buildSsoUrl(startUrl, { returnTo, companySlug } = {}) {
|
|
139
|
+
const raw = String(startUrl != null ? startUrl : "");
|
|
140
|
+
const hashAt = raw.indexOf("#");
|
|
141
|
+
const hash = hashAt === -1 ? "" : raw.slice(hashAt);
|
|
142
|
+
const beforeHash = hashAt === -1 ? raw : raw.slice(0, hashAt);
|
|
143
|
+
const queryAt = beforeHash.indexOf("?");
|
|
144
|
+
const path = queryAt === -1 ? beforeHash : beforeHash.slice(0, queryAt);
|
|
145
|
+
const query = queryAt === -1 ? "" : beforeHash.slice(queryAt + 1);
|
|
146
|
+
const add = [
|
|
147
|
+
["return_to", returnTo],
|
|
148
|
+
["dt_company", companySlug]
|
|
149
|
+
].filter(([, v]) => v != null && v !== "");
|
|
150
|
+
const replaced = new Set(add.map(([k]) => k));
|
|
151
|
+
const params = query.split("&").filter((p) => p && !replaced.has(decodeKey(p))).concat(add.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`));
|
|
152
|
+
return `${path}${params.length ? `?${params.join("&")}` : ""}${hash}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/icons.jsx
|
|
126
156
|
var import_react2 = require("react");
|
|
157
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
158
|
+
var base = {
|
|
159
|
+
viewBox: "0 0 24 24",
|
|
160
|
+
fill: "none",
|
|
161
|
+
stroke: "currentColor",
|
|
162
|
+
strokeWidth: 2,
|
|
163
|
+
strokeLinecap: "round",
|
|
164
|
+
strokeLinejoin: "round",
|
|
165
|
+
"aria-hidden": "true",
|
|
166
|
+
focusable: "false"
|
|
167
|
+
};
|
|
168
|
+
var make = (paths) => function Icon({ size = 16, className = "dt-lp-icon" }) {
|
|
169
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { ...base, width: size, height: size, className, children: paths });
|
|
170
|
+
};
|
|
171
|
+
var ArrowRight = make(/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M5 12h14M13 6l6 6-6 6" }));
|
|
172
|
+
var ChevronRight = make(/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "m9 6 6 6-6 6" }));
|
|
173
|
+
var Check = make(/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M20 6 9 17l-5-5" }));
|
|
174
|
+
var Help = make(
|
|
175
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
176
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
|
|
177
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3M12 17h.01" })
|
|
178
|
+
] })
|
|
179
|
+
);
|
|
180
|
+
var Alert = make(
|
|
181
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
182
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z" }),
|
|
183
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 9v4M12 17h.01" })
|
|
184
|
+
] })
|
|
185
|
+
);
|
|
186
|
+
var MenuIcon = make(/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M4 6h16M4 12h16M4 18h16" }));
|
|
187
|
+
var Close = make(/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M18 6 6 18M6 6l12 12" }));
|
|
188
|
+
var Calendar = make(
|
|
189
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
190
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "3", y: "4", width: "18", height: "18", rx: "2" }),
|
|
191
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M16 2v4M8 2v4M3 10h18m-12 6 2 2 4-4" })
|
|
192
|
+
] })
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// src/DreamtreeSso.jsx
|
|
196
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
197
|
+
var DEFAULT_SSO_LABEL = "Continue with Dreamtree";
|
|
198
|
+
function DreamtreeSso({ provider, children }) {
|
|
199
|
+
const { startUrl, mode, label = DEFAULT_SSO_LABEL, companySlug, returnTo } = provider;
|
|
200
|
+
const isSso = mode === "sso";
|
|
201
|
+
const autoRedirect = isSso && provider.autoRedirect !== false;
|
|
202
|
+
const href = buildSsoUrl(startUrl, { returnTo, companySlug });
|
|
203
|
+
const redirected = (0, import_react3.useRef)(false);
|
|
204
|
+
(0, import_react3.useEffect)(() => {
|
|
205
|
+
if (!autoRedirect || redirected.current) return;
|
|
206
|
+
redirected.current = true;
|
|
207
|
+
window.location.assign(href);
|
|
208
|
+
}, [autoRedirect, href]);
|
|
209
|
+
const button = /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("a", { className: "dt-auth-sso-btn", href, children: [
|
|
210
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: label }),
|
|
211
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ArrowRight, { size: 16, className: "dt-auth-sso-icon" })
|
|
212
|
+
] });
|
|
213
|
+
if (isSso) {
|
|
214
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dt-auth-sso dt-auth-sso--only", children: [
|
|
215
|
+
autoRedirect ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "dt-auth-sso-status", role: "status", "aria-live": "polite", children: [
|
|
216
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dt-auth-sso-spinner", "aria-hidden": "true" }),
|
|
217
|
+
"Redirecting to Dreamtree ID\u2026"
|
|
218
|
+
] }) : null,
|
|
219
|
+
button
|
|
220
|
+
] });
|
|
221
|
+
}
|
|
222
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
223
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dt-auth-sso", children: button }),
|
|
224
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dt-auth-or", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "or" }) }),
|
|
225
|
+
children
|
|
226
|
+
] });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/useTapReveal.js
|
|
230
|
+
var import_react4 = require("react");
|
|
127
231
|
function useTapReveal({ taps = 4, windowMs = 1500 } = {}) {
|
|
128
|
-
const [revealed, setRevealed] = (0,
|
|
129
|
-
const count = (0,
|
|
130
|
-
const last = (0,
|
|
131
|
-
const onClick = (0,
|
|
232
|
+
const [revealed, setRevealed] = (0, import_react4.useState)(false);
|
|
233
|
+
const count = (0, import_react4.useRef)(0);
|
|
234
|
+
const last = (0, import_react4.useRef)(0);
|
|
235
|
+
const onClick = (0, import_react4.useCallback)(() => {
|
|
132
236
|
if (revealed) return;
|
|
133
237
|
const now = Date.now();
|
|
134
238
|
count.current = now - last.current <= windowMs ? count.current + 1 : 1;
|
|
@@ -139,7 +243,7 @@ function useTapReveal({ taps = 4, windowMs = 1500 } = {}) {
|
|
|
139
243
|
}
|
|
140
244
|
|
|
141
245
|
// src/AuthShell.jsx
|
|
142
|
-
var
|
|
246
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
143
247
|
var DEFAULT_LEGAL = [
|
|
144
248
|
{ label: "Privacy Policy", href: "/privacy" },
|
|
145
249
|
{ label: "Terms", href: "/terms" },
|
|
@@ -147,10 +251,10 @@ var DEFAULT_LEGAL = [
|
|
|
147
251
|
];
|
|
148
252
|
function BrandMark({ brand, size }) {
|
|
149
253
|
if (brand.logoSrc) {
|
|
150
|
-
return /* @__PURE__ */ (0,
|
|
254
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("img", { className: "dt-auth-mark-img", src: brand.logoSrc, alt: "", width: size, height: size });
|
|
151
255
|
}
|
|
152
|
-
if (brand.mark) return /* @__PURE__ */ (0,
|
|
153
|
-
return /* @__PURE__ */ (0,
|
|
256
|
+
if (brand.mark) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dt-auth-mark-node", children: brand.mark });
|
|
257
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dt-auth-monogram", style: { width: size, height: size }, "aria-hidden": "true", children: brand.name.charAt(0) });
|
|
154
258
|
}
|
|
155
259
|
function brandStyle(brand) {
|
|
156
260
|
return {
|
|
@@ -169,52 +273,53 @@ function AuthShell({
|
|
|
169
273
|
legalLinks = DEFAULT_LEGAL,
|
|
170
274
|
linkComponent: LinkComponent,
|
|
171
275
|
className = "",
|
|
172
|
-
demoAccounts
|
|
276
|
+
demoAccounts,
|
|
277
|
+
ssoProvider
|
|
173
278
|
}) {
|
|
174
279
|
var _a, _b;
|
|
175
|
-
const headingId = (0,
|
|
280
|
+
const headingId = (0, import_react5.useId)();
|
|
176
281
|
const { revealed, tapProps } = useTapReveal({ taps: (_a = demoAccounts == null ? void 0 : demoAccounts.taps) != null ? _a : 4 });
|
|
177
282
|
const isPlatform = variant === "platform";
|
|
178
283
|
const headline = isPlatform ? brand.platformHeadline || "Platform administration" : brand.headline;
|
|
179
284
|
const pitch = isPlatform ? brand.platformPitch || `Super administrator console for ${brand.name}. Every action here is audited.` : brand.pitch;
|
|
180
|
-
const renderLink = (l) => LinkComponent ? /* @__PURE__ */ (0,
|
|
181
|
-
return /* @__PURE__ */ (0,
|
|
285
|
+
const renderLink = (l) => LinkComponent ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(LinkComponent, { to: l.href, href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("a", { href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href);
|
|
286
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
182
287
|
"div",
|
|
183
288
|
{
|
|
184
289
|
className: `dt-auth ${isPlatform ? "dt-auth--platform" : ""} ${className}`,
|
|
185
290
|
style: brandStyle(brand),
|
|
186
291
|
"data-product": brand.id,
|
|
187
292
|
children: [
|
|
188
|
-
/* @__PURE__ */ (0,
|
|
189
|
-
/* @__PURE__ */ (0,
|
|
190
|
-
/* @__PURE__ */ (0,
|
|
191
|
-
/* @__PURE__ */ (0,
|
|
192
|
-
isPlatform ? /* @__PURE__ */ (0,
|
|
293
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("aside", { className: "dt-auth-panel", children: [
|
|
294
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("a", { href: "/", className: "dt-auth-brand", children: [
|
|
295
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BrandMark, { brand, size: 28 }),
|
|
296
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dt-auth-brand-name", children: brand.name }),
|
|
297
|
+
isPlatform ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dt-auth-badge", children: "Platform" }) : null
|
|
193
298
|
] }),
|
|
194
|
-
/* @__PURE__ */ (0,
|
|
195
|
-
/* @__PURE__ */ (0,
|
|
196
|
-
pitch ? /* @__PURE__ */ (0,
|
|
197
|
-
!isPlatform && ((_b = brand.points) == null ? void 0 : _b.length) ? /* @__PURE__ */ (0,
|
|
299
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dt-auth-pitch", children: [
|
|
300
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h2", { children: headline }),
|
|
301
|
+
pitch ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { children: pitch }) : null,
|
|
302
|
+
!isPlatform && ((_b = brand.points) == null ? void 0 : _b.length) ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("ul", { children: brand.points.map((p) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("li", { children: p }, p)) }) : null
|
|
198
303
|
] }),
|
|
199
|
-
/* @__PURE__ */ (0,
|
|
304
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dt-auth-panel-foot", children: brand.category || "by Dreamtree-Org" })
|
|
200
305
|
] }),
|
|
201
|
-
/* @__PURE__ */ (0,
|
|
202
|
-
/* @__PURE__ */ (0,
|
|
203
|
-
/* @__PURE__ */ (0,
|
|
204
|
-
/* @__PURE__ */ (0,
|
|
205
|
-
isPlatform ? /* @__PURE__ */ (0,
|
|
306
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("main", { className: "dt-auth-main", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dt-auth-column", children: [
|
|
307
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("a", { href: "/", className: "dt-auth-brand dt-auth-brand--mobile", children: [
|
|
308
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BrandMark, { brand, size: 24 }),
|
|
309
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dt-auth-brand-name", children: brand.name }),
|
|
310
|
+
isPlatform ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dt-auth-badge", children: "Platform" }) : null
|
|
206
311
|
] }),
|
|
207
|
-
/* @__PURE__ */ (0,
|
|
208
|
-
/* @__PURE__ */ (0,
|
|
209
|
-
/* @__PURE__ */ (0,
|
|
210
|
-
subtitle ? /* @__PURE__ */ (0,
|
|
312
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("section", { className: "dt-auth-card", "aria-labelledby": headingId, children: [
|
|
313
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("header", { className: "dt-auth-head", children: [
|
|
314
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h1", { id: headingId, ...demoAccounts ? tapProps : {}, children: title }),
|
|
315
|
+
subtitle ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { children: subtitle }) : null
|
|
211
316
|
] }),
|
|
212
|
-
children,
|
|
213
|
-
demoAccounts && revealed ? /* @__PURE__ */ (0,
|
|
317
|
+
ssoProvider ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DreamtreeSso, { provider: ssoProvider, children }) : children,
|
|
318
|
+
demoAccounts && revealed && (ssoProvider == null ? void 0 : ssoProvider.mode) !== "sso" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DemoAccounts, { load: demoAccounts.load, onSelect: demoAccounts.onSelect }) : null
|
|
214
319
|
] }),
|
|
215
|
-
footer ? /* @__PURE__ */ (0,
|
|
216
|
-
(legalLinks == null ? void 0 : legalLinks.length) ? /* @__PURE__ */ (0,
|
|
217
|
-
i > 0 ? /* @__PURE__ */ (0,
|
|
320
|
+
footer ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dt-auth-footer", children: footer }) : null,
|
|
321
|
+
(legalLinks == null ? void 0 : legalLinks.length) ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("nav", { className: "dt-auth-legal", "aria-label": "Legal", children: legalLinks.map((l, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "dt-auth-legal-item", children: [
|
|
322
|
+
i > 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { "aria-hidden": "true", children: "\xB7" }) : null,
|
|
218
323
|
renderLink(l)
|
|
219
324
|
] }, l.href)) }) : null
|
|
220
325
|
] }) })
|