@graph8/sdk 0.373.0 → 0.374.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 +3 -3
- package/blocks/_foundation/README.md +47 -0
- package/blocks/_foundation/block.json +13 -0
- package/blocks/_foundation/components/auth-gate.tsx +363 -0
- package/blocks/_foundation/server/auth.ts +815 -0
- package/blocks/campaigns/README.md +1 -1
- package/blocks/campaigns/block.json +34 -1
- package/blocks/campaigns/components/campaign-links.tsx +180 -0
- package/blocks/company-intelligence/components/research-run.tsx +6 -2
- package/blocks/crm/README.md +19 -0
- package/blocks/crm/block.json +205 -6
- package/blocks/crm/components/bulk-update.tsx +119 -0
- package/blocks/crm/components/contact-documents-tab.tsx +144 -0
- package/blocks/crm/components/contact-insights-tab.tsx +118 -0
- package/blocks/crm/components/contact-lookup-link.tsx +47 -6
- package/blocks/crm/components/contact-row-actions.tsx +43 -0
- package/blocks/crm/components/contact-tag-chips.tsx +113 -0
- package/blocks/crm/components/contacts-screen.tsx +46 -5
- package/blocks/crm/components/owner-assign.tsx +132 -0
- package/blocks/crm/components/record-open-links.tsx +79 -0
- package/blocks/crm/components/tag-assign.tsx +112 -0
- package/blocks/crm/components/tag-manager.tsx +178 -0
- package/blocks/crm/components/unlock-info-button.tsx +82 -0
- package/blocks/crm/components/verify-email-button.tsx +93 -0
- package/blocks/crm/hooks/use-contact-tags.ts +29 -0
- package/blocks/crm/lib/record-actions.ts +102 -0
- package/blocks/deals/README.md +1 -0
- package/blocks/deals/block.json +17 -0
- package/blocks/deals/components/create-deal-button.tsx +109 -0
- package/blocks/deals/components/deal-detail.tsx +5 -3
- package/blocks/dialer/components/call-log-screen.tsx +6 -1
- package/blocks/documents/README.md +86 -0
- package/blocks/documents/block.json +378 -0
- package/blocks/documents/components/account-bar.tsx +48 -0
- package/blocks/documents/components/correspondence-form.tsx +95 -0
- package/blocks/documents/components/correspondence-import.tsx +106 -0
- package/blocks/documents/components/correspondence-panel.tsx +191 -0
- package/blocks/documents/components/documents-screen.tsx +76 -0
- package/blocks/documents/components/library-document-detail.tsx +175 -0
- package/blocks/documents/components/library-document-form.tsx +105 -0
- package/blocks/documents/components/library-folder-form.tsx +74 -0
- package/blocks/documents/components/library-panel.tsx +235 -0
- package/blocks/documents/components/library-upload.tsx +160 -0
- package/blocks/documents/components/question-row.tsx +130 -0
- package/blocks/documents/components/questionnaire-detail.tsx +197 -0
- package/blocks/documents/components/questionnaires-panel.tsx +173 -0
- package/blocks/documents/components/record-documents-tab.tsx +109 -0
- package/blocks/documents/lib/documents-types.ts +252 -0
- package/blocks/documents/lib/download.ts +64 -0
- package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
- package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
- package/blocks/documents/recipes/organize-the-library.ts +25 -0
- package/blocks/email-signatures/README.md +123 -0
- package/blocks/email-signatures/block.json +341 -0
- package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
- package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
- package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
- package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
- package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
- package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
- package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
- package/blocks/email-signatures/components/signature-policy.tsx +130 -0
- package/blocks/email-signatures/components/signature-preview.tsx +119 -0
- package/blocks/email-signatures/components/signature-report.tsx +77 -0
- package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
- package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
- package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
- package/blocks/email-signatures/lib/signatures-types.ts +392 -0
- package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
- package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
- package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
- package/blocks/enrichment/README.md +22 -4
- package/blocks/enrichment/block.json +145 -5
- package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
- package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +7 -7
- package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
- package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +34 -3
- package/blocks/enrichment/components/list-pipelines.tsx +97 -0
- package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
- package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
- package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
- package/blocks/enrichment/lib/enrichment-types.ts +12 -0
- package/blocks/enrichment/lib/pipeline-types.ts +234 -0
- package/blocks/forms/README.md +2 -0
- package/blocks/forms/components/forms-screen.tsx +6 -3
- package/blocks/inbox/README.md +40 -4
- package/blocks/inbox/block.json +130 -8
- package/blocks/inbox/components/blocked-senders.tsx +103 -0
- package/blocks/inbox/components/compose-message.tsx +193 -0
- package/blocks/inbox/components/inbox-screen.tsx +54 -6
- package/blocks/inbox/components/inbox-search.tsx +106 -0
- package/blocks/inbox/components/tag-manager.tsx +136 -0
- package/blocks/inbox/components/thread-actions.tsx +133 -0
- package/blocks/inbox/components/thread-view.tsx +2 -0
- package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
- package/blocks/inbox/lib/inbox-manage.ts +227 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +3 -1
- package/blocks/pages/block.json +10 -1
- package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
- package/blocks/programmatic-pages/README.md +82 -0
- package/blocks/programmatic-pages/block.json +292 -0
- package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
- package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
- package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
- package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
- package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
- package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
- package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
- package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
- package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
- package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
- package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
- package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
- package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
- package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
- package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
- package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
- package/blocks/quotes/README.md +25 -1
- package/blocks/quotes/block.json +123 -6
- package/blocks/quotes/components/manual-products.tsx +144 -0
- package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
- package/blocks/quotes/components/quote-documents.tsx +94 -0
- package/blocks/quotes/components/quote-evidence.tsx +95 -0
- package/blocks/quotes/components/quote-list.tsx +23 -5
- package/blocks/quotes/components/quote-settings.tsx +184 -0
- package/blocks/quotes/components/quote-templates.tsx +189 -0
- package/blocks/quotes/components/quote-viewer.tsx +16 -3
- package/blocks/quotes/components/quotes-screen.tsx +15 -5
- package/blocks/quotes/lib/quote-admin.ts +264 -0
- package/blocks/sequencer/README.md +55 -0
- package/blocks/sequencer/block.json +172 -5
- package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
- package/blocks/sequencer/components/sequence-builder.tsx +13 -3
- package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
- package/blocks/sequencer/components/sequence-detail.tsx +6 -0
- package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +2 -1
- package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
- package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
- package/blocks/sequencer/components/sequence-templates.tsx +160 -0
- package/blocks/sequencer/components/sequences-screen.tsx +36 -6
- package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
- package/blocks/sequencer/components/step-editor.tsx +10 -1
- package/blocks/sequencer/components/step-templates.tsx +224 -0
- package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +4 -0
- package/blocks/signals/README.md +39 -0
- package/blocks/signals/block.json +287 -0
- package/blocks/signals/components/auto-seed-panel.tsx +111 -0
- package/blocks/signals/components/listener-form.tsx +184 -0
- package/blocks/signals/components/matched-posts.tsx +195 -0
- package/blocks/signals/components/post-engagements.tsx +114 -0
- package/blocks/signals/components/record-social-tab.tsx +49 -0
- package/blocks/signals/components/social-listeners-screen.tsx +250 -0
- package/blocks/signals/components/social-settings.tsx +128 -0
- package/blocks/signals/components/source-lists.tsx +83 -0
- package/blocks/signals/components/top-engagers.tsx +68 -0
- package/blocks/signals/components/x-integration.tsx +129 -0
- package/blocks/signals/lib/signals-types.ts +228 -0
- package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
- package/blocks/signals/recipes/spend-against-caps.ts +14 -0
- package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
- package/blocks/visitors/components/traffic-chart.tsx +3 -2
- package/blocks/web-analytics/README.md +102 -0
- package/blocks/web-analytics/block.json +292 -0
- package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
- package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
- package/blocks/web-analytics/components/audience-tab.tsx +135 -0
- package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
- package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
- package/blocks/web-analytics/components/overview-tab.tsx +102 -0
- package/blocks/web-analytics/components/pages-tab.tsx +148 -0
- package/blocks/web-analytics/components/ranked-table.tsx +103 -0
- package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
- package/blocks/web-analytics/components/report-card.tsx +71 -0
- package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
- package/blocks/web-analytics/hooks/use-report.ts +48 -0
- package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
- package/blocks/web-analytics/lib/page-path.ts +14 -0
- package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
- package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
- package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
- package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
- package/blocks/workbench/README.md +54 -2
- package/blocks/workbench/block.json +234 -5
- package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
- package/blocks/workbench/components/workbench-automation.tsx +36 -0
- package/blocks/workbench/components/workbench-connections.tsx +136 -0
- package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
- package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
- package/blocks/workbench/components/workbench-grid.tsx +70 -7
- package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
- package/blocks/workbench/components/workbench-schedule.tsx +191 -0
- package/blocks/workbench/components/workbench-screen.tsx +50 -17
- package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
- package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
- package/blocks/workbench/components/workbench-triggers.tsx +178 -0
- package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
- package/blocks/workbench/lib/api-status.ts +8 -0
- package/blocks/workbench/lib/automation-types.ts +257 -0
- package/dist/cli.js +257 -51
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +1 -1
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +1 -1
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/auth.d.ts +45 -0
- package/dist/types/blocks/create.d.ts +5 -2
- package/dist/types/version.d.ts +2 -2
- package/docs/api/forms.md +1 -1
- package/package.json +1 -1
- package/stacks/6sense/pages/contacts.tsx.tmpl +2 -0
- package/stacks/6sense.json +20 -0
- package/stacks/apollo/pages/calls.tsx.tmpl +9 -9
- package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +6 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +3 -3
- package/stacks/apollo/pages/lists.tsx.tmpl +14 -2
- package/stacks/apollo/pages/sequences.tsx.tmpl +8 -2
- package/stacks/apollo.json +33 -2
- package/stacks/clay/pages/contacts.tsx.tmpl +2 -0
- package/stacks/clay/pages/lists.tsx.tmpl +14 -2
- package/stacks/gong/pages/calls.tsx.tmpl +9 -9
- package/stacks/gong/pages/contacts.tsx.tmpl +2 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +2 -0
- package/stacks/hootsuite.json +10 -0
- package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +2 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +6 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +4 -0
- package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +2 -2
- package/stacks/hubspot/pages/sequences.tsx.tmpl +8 -2
- package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
- package/stacks/hubspot.json +45 -2
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +3 -3
- package/stacks/intercom.json +10 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +2 -0
- package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
- package/stacks/mailchimp.json +2 -1
- package/stacks/outreach/pages/calls.tsx.tmpl +9 -9
- package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +2 -0
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +3 -3
- package/stacks/outreach/pages/sequences.tsx.tmpl +8 -2
- package/stacks/outreach.json +13 -2
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +2 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +2 -2
- package/stacks/pipedrive.json +10 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +2 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +6 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +4 -0
- package/stacks/salesforce.json +10 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +4 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +14 -2
- package/stacks/zoominfo.json +20 -0
- package/styles/styles.css +1 -1
- package/templates/auth/auth-route.ts.tmpl +16 -0
- package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
- package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
- package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
- package/templates/auth/g8.auth.password.ts.tmpl +18 -0
- package/templates/auth/g8.auth.session.ts.tmpl +23 -0
- package/templates/auth/middleware.ts.tmpl +14 -0
- package/templates/next-app/README.md.tmpl +5 -0
package/README.md
CHANGED
|
@@ -499,10 +499,10 @@ npx @graph8/sdk create mailchimp my-app # then: cd my-app && cp .env.local.exa
|
|
|
499
499
|
| `mailchimp` | Newsletter, nurture, forms, contacts, companies, tasks, lists, suppressions |
|
|
500
500
|
| `clay` | Prospect search, workbench, enrichment, lists, contacts, companies, tasks, suppressions |
|
|
501
501
|
| `6sense` | Intent, visitors, competitors, companies, contacts, tasks |
|
|
502
|
-
| `intercom` | Inbox, web chat conversations, contacts, companies, tasks, AI agents, mailboxes |
|
|
502
|
+
| `intercom` | Inbox, web chat conversations, contacts, companies, tasks, AI agents, mailboxes, email signatures |
|
|
503
503
|
| `calendly` | Meetings and scheduling (event types, availability, booking pages) over contacts, companies and tasks |
|
|
504
|
-
| `pipedrive` | Analytics, deals, quotes, contacts, companies, inbox, meetings, scheduling, tasks and
|
|
505
|
-
| `outreach` | Analytics, contacts, companies, lists, sequences, campaigns, inbox, dialer, call log, meetings, scheduling, tasks, mailboxes, suppressions |
|
|
504
|
+
| `pipedrive` | Analytics, deals, quotes, contacts, companies, inbox, meetings, scheduling, tasks, mailboxes and email signatures; deals and quotes open each other, replies can start a deal |
|
|
505
|
+
| `outreach` | Analytics, contacts, companies, lists, sequences, campaigns, inbox, dialer, call log, meetings, scheduling, tasks, mailboxes, email signatures, suppressions |
|
|
506
506
|
| `apollo` | Outreach's screens plus prospect search, enrichment, intent, visitors, deals and integrations; contacts get Enrich, Add to sequence and a Deals tab |
|
|
507
507
|
| `gong` | Analytics, call log, coaching, dialer, meetings, scheduling, contacts, companies, tasks, lists, suppressions |
|
|
508
508
|
| `salesforce` | Analytics, contacts, companies, lists, deals, quotes, lifecycle, tasks, workflows, copilot, custom objects, suppressions, integrations |
|
|
@@ -10,6 +10,8 @@ Shared base every other block depends on. Install it first (the CLI does it for
|
|
|
10
10
|
| `server/proxy.ts` | `createG8Proxy(config)`: the logic of the proxy (forwarding, allowlist, `authorize` hook). |
|
|
11
11
|
| `server/route.ts` | Installs as `app/api/g8/[...path]/route.ts` and mounts the proxy. `allowedPrefixes` is generated from the installed blocks' operations; local preview is on. |
|
|
12
12
|
| `server/local-key.ts`, `server/connect.ts`, `server/connect-route.ts` | Local preview key store: `app/api/g8-connect/route.ts` checks a pasted key (GET /me) and keeps it in `.g8/local-key` on the server. 404 in production. |
|
|
13
|
+
| `server/auth.ts` | Optional `authorize` presets (sign-in) for the proxy: `sharedPassword`, `basicAuth`, `jwtAuth`, `sessionAuth`, `allowAnonymous`, and `createG8AuthRoute`. See "Sign-in presets" below. |
|
|
14
|
+
| `components/auth-gate.tsx` | `G8AuthGate`, `SignInPanel`, `G8SignOutButton`: the branded sign-in screen for apps that use a preset. |
|
|
13
15
|
| `components/connect-graph8.tsx` | `ConnectGraph8Panel` + `G8ConnectGate`: the first-run Connect graph8 panel in `next dev`; a no-op in production. |
|
|
14
16
|
| `hooks/use-g8-query.ts` | `useG8Query(fetcher, deps)` -> `{ data, error, loading, refetch }`. Latest request wins. |
|
|
15
17
|
| `hooks/use-g8-mutation.ts` | `useG8Mutation(fn)` -> `{ mutate, mutateAsync, data, error, loading, reset }`. |
|
|
@@ -122,6 +124,51 @@ Connect graph8 panel, which the server checks against `GET /me` and stores in `.
|
|
|
122
124
|
`npx @graph8/sdk doctor --production` fails until the route is safe to deploy;
|
|
123
125
|
`npx @graph8/sdk doctor --fix-prefixes` regenerates `allowedPrefixes` after you add or remove blocks.
|
|
124
126
|
|
|
127
|
+
### Sign-in presets (optional): going live with `authorize`
|
|
128
|
+
|
|
129
|
+
`authorize` is any function `(request) => Response | void`; your own session check is always fine. For the
|
|
130
|
+
common cases `server/auth.ts` ships presets, and `npx @graph8/sdk create <stack> <dir> --auth <preset>` wires one
|
|
131
|
+
up (g8.auth.ts, the route's `authorize`, the sign-in screen, README and `.env.local.example`). Nothing changes for
|
|
132
|
+
apps that do not use one: without `authorize` the proxy behaves exactly as described above.
|
|
133
|
+
|
|
134
|
+
| Preset | What it checks | Server environment |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| `sharedPassword()` | one team password; signs an HttpOnly, SameSite=Lax session cookie (HMAC-SHA-256, 7 days); sign-in screen | `G8_APP_PASSWORD` (12+), `G8_SESSION_SECRET` (32+) |
|
|
137
|
+
| `basicAuth()` | HTTP Basic auth; pair with a `middleware.ts` so every page asks; refuses cross-site writes; 20 wrong tries per client per 15 min | `G8_BASIC_AUTH` (`user:pass,user2:pass2`; passwords 12+ characters, no commas) |
|
|
138
|
+
| `jwtAuth()` | a JWT from your OIDC provider (JWKS: RS/PS/ES256 family, discovered from the issuer) or an HS256 secret; exp, nbf, iss, aud; optional `claims` check | `G8_JWT_ISSUER`, `G8_JWT_JWKS_URL`, `G8_JWT_AUDIENCE`, `G8_JWT_COOKIE` or a Bearer header, `G8_JWT_SECRET` |
|
|
139
|
+
| `sessionAuth(fn)` | your session library: return the user to allow (Clerk `auth()`, NextAuth `auth()`, Supabase `getUser()`) | `G8_SIGN_IN_URL`, `G8_SIGN_OUT_URL` (for the screen) |
|
|
140
|
+
| `allowAnonymous()` | nothing: anyone who can reach the app uses the key. Logs a warning; `doctor` flags it | |
|
|
141
|
+
|
|
142
|
+
Wire one by hand in an existing app:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
// g8.auth.ts
|
|
146
|
+
import { sharedPassword } from "@/components/g8/_foundation/server/auth";
|
|
147
|
+
export const appAuth = sharedPassword();
|
|
148
|
+
|
|
149
|
+
// app/api/g8/[...path]/route.ts: inside createG8Proxy({ ... })
|
|
150
|
+
authorize: appAuth.authorize,
|
|
151
|
+
|
|
152
|
+
// app/api/g8-auth/route.ts (the sign-in screen's server side; password, jwt and session presets)
|
|
153
|
+
import { createG8AuthRoute } from "@/components/g8/_foundation/server/auth";
|
|
154
|
+
import { appAuth } from "@/g8.auth";
|
|
155
|
+
export const { GET, POST, DELETE } = createG8AuthRoute(appAuth);
|
|
156
|
+
|
|
157
|
+
// app/layout.tsx: wrap the shell (BrandProvider keeps the screen on brand)
|
|
158
|
+
<BrandProvider brand={brand}><G8AuthGate><AppShell headerSlot={<G8SignOutButton />}>...</AppShell></G8AuthGate></BrandProvider>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Security: passwords and secrets are compared in constant time; cookie and Basic presets refuse cross-site writes (CSRF:
|
|
162
|
+
`Sec-Fetch-Site`/`Origin`); JWTs with alg `none`, an algorithm outside the allow-list, a key of the wrong type
|
|
163
|
+
(no HS/RS confusion), `crit` headers or no `exp` are refused; sign-in attempts are rate limited per client (in
|
|
164
|
+
memory: best effort on serverless, so pick a long password). `sharedPassword` also caps WRONG passwords from all
|
|
165
|
+
clients together (`maxFailuresGlobal`, default 100 per 15 min): anyone who can reach the sign-in route can keep
|
|
166
|
+
NEW sign-ins paused that way (existing sessions keep working); with a long random password raise it or pass
|
|
167
|
+
`Infinity`. `jwtAuth` with a JWKS and no audience accepts any token the issuer minted for other apps on the same
|
|
168
|
+
tenant: set `G8_JWT_AUDIENCE` or a `claims` check (it warns at startup, and `doctor` warns). `sharedPassword` and
|
|
169
|
+
`basicAuth` read the environment on every request; `jwtAuth` and `sessionAuth` once at startup. A missing setting is a 500 that names the variable,
|
|
170
|
+
never its value. The sign-in screen is UX only: the proxy's `authorize` is the gate.
|
|
171
|
+
|
|
125
172
|
What the proxy does on its own: rejects `..`, encoded slashes and empty segments (400); forwards only
|
|
126
173
|
`content-type`, `accept`, `accept-language` and `idempotency-key` (never the browser's cookies or
|
|
127
174
|
`Authorization`); strips hop-by-hop headers, `set-cookie` and `location` from the response; does not follow
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"spend/send/delete confirmation dialog",
|
|
10
10
|
"screen kit: layout, stats, split view, detail panel, toolbar, status pills, avatars, cards, form sheet, charts",
|
|
11
11
|
"local preview: a Connect graph8 screen stores the server's key for `next dev`; generated allowedPrefixes",
|
|
12
|
+
"go live: optional sign-in presets for the proxy's authorize (team password, Basic, OIDC/JWT, Clerk/NextAuth/Supabase sessions) and a branded sign-in screen",
|
|
12
13
|
"white-label: BrandProvider + useBrand give every block's copy and link-outs your platform name and app URL (default graph8)"
|
|
13
14
|
],
|
|
14
15
|
"dependsOn": [],
|
|
@@ -149,6 +150,13 @@
|
|
|
149
150
|
"operations": [],
|
|
150
151
|
"tiers": []
|
|
151
152
|
},
|
|
153
|
+
{
|
|
154
|
+
"name": "G8AuthGate",
|
|
155
|
+
"file": "components/auth-gate.tsx",
|
|
156
|
+
"purpose": "Sign-in for apps with an auth preset (create --auth): G8AuthGate shows SignInPanel (team password form, provider redirect, or the missing server setting) until the visitor is signed in, G8SignOutButton for the header. Talks only to the app's /api/g8-auth route; renders the app unchanged when that route does not exist.",
|
|
157
|
+
"operations": [],
|
|
158
|
+
"tiers": []
|
|
159
|
+
},
|
|
152
160
|
{
|
|
153
161
|
"name": "BrandProvider",
|
|
154
162
|
"file": "components/brand-provider.tsx",
|
|
@@ -270,6 +278,11 @@
|
|
|
270
278
|
"purpose": "createG8Proxy(): forwards browser calls to the graph8 API with the server-side key, a required path allowlist and authorize hook (it refuses to run without them) and a server-side org hook.",
|
|
271
279
|
"target": "g8/server/proxy.ts"
|
|
272
280
|
},
|
|
281
|
+
{
|
|
282
|
+
"file": "server/auth.ts",
|
|
283
|
+
"purpose": "Optional authorize presets for the proxy: sharedPassword (signed session cookie + sign-in route), basicAuth, jwtAuth (OIDC JWKS or HS256), sessionAuth (Clerk/NextAuth/Supabase adapter), allowAnonymous; createG8AuthRoute() backs the sign-in screen. Web APIs only (Node and Edge).",
|
|
284
|
+
"target": "g8/server/auth.ts"
|
|
285
|
+
},
|
|
273
286
|
{
|
|
274
287
|
"file": "server/local-key.ts",
|
|
275
288
|
"purpose": "Node only: the local-preview key file (.g8/local-key, 0600, self-gitignored) and resolveApiKey() (environment first, then the file outside production).",
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sign-in for a created app that uses an auth preset (`create --auth <preset>`, g8/server/auth.ts). The proxy
|
|
5
|
+
* enforces sign-in on the server; this is the screen people see instead of empty, refused data.
|
|
6
|
+
*
|
|
7
|
+
* Calls this app's own `/api/g8-auth` route (createG8AuthRoute), never graph8:
|
|
8
|
+
* GET the session status: signed in or not, which preset, the sign-in URL, or what the server is missing
|
|
9
|
+
* POST { password } (shared-password preset): sets an HttpOnly session cookie
|
|
10
|
+
* DELETE sign out (shared-password preset)
|
|
11
|
+
* No billable, external or destructive graph8 operation. The password goes from the input to the server once
|
|
12
|
+
* and is never kept in browser state after submit, storage or the URL.
|
|
13
|
+
*
|
|
14
|
+
* Exports:
|
|
15
|
+
* - G8AuthGate: wraps the app. Shows a loading screen while it checks, SignInPanel when signed out, the app when
|
|
16
|
+
* signed in. Re-checks when the tab becomes visible again. If /api/g8-auth does not exist it renders the
|
|
17
|
+
* app unchanged (apps without a preset).
|
|
18
|
+
* - SignInPanel: the sign-in screen (password form, provider redirect, or the setup problem). No required props.
|
|
19
|
+
* - G8SignOutButton: a header button (AppShell `headerSlot`); renders nothing outside a signed-in gate.
|
|
20
|
+
* - useG8Auth: { status, signOut, refresh } inside the gate, else null.
|
|
21
|
+
* The app's name and logo come from the nearest BrandProvider (g8.config.ts), the colours from the theme tokens
|
|
22
|
+
* (the Brand Kit region in globals.css), so the screen is on brand without edits.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { createContext, useCallback, useContext, useEffect, useId, useRef, useState, type FormEvent, type ReactNode } from "react";
|
|
26
|
+
import { AlertTriangle, ArrowRight, Eye, EyeOff, Loader2, LockKeyhole, LogIn, LogOut, RefreshCw } from "lucide-react";
|
|
27
|
+
import { Button, Input, cn } from "@graph8/sdk/ui";
|
|
28
|
+
import { useBrand } from "../hooks/use-brand";
|
|
29
|
+
|
|
30
|
+
export const AUTH_PATH = "/api/g8-auth";
|
|
31
|
+
|
|
32
|
+
export type AuthKind = "password" | "basic" | "jwt" | "session" | "anonymous";
|
|
33
|
+
|
|
34
|
+
export interface AuthStatus {
|
|
35
|
+
kind: AuthKind;
|
|
36
|
+
signedIn: boolean;
|
|
37
|
+
signInUrl?: string;
|
|
38
|
+
signOutUrl?: string;
|
|
39
|
+
problem?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The status, null when the route does not exist (no preset), or "error" when the server could not be reached. */
|
|
43
|
+
export async function fetchAuthStatus(): Promise<AuthStatus | null | "error"> {
|
|
44
|
+
try {
|
|
45
|
+
const res = await fetch(AUTH_PATH, { headers: { accept: "application/json" }, cache: "no-store", credentials: "same-origin" });
|
|
46
|
+
if (res.status === 404) return null;
|
|
47
|
+
if (!res.ok) return "error";
|
|
48
|
+
const body = (await res.json().catch(() => null)) as AuthStatus | null;
|
|
49
|
+
return body && typeof body.signedIn === "boolean" ? body : "error";
|
|
50
|
+
} catch {
|
|
51
|
+
return "error";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface AuthContextValue {
|
|
56
|
+
status: AuthStatus;
|
|
57
|
+
signOut: () => Promise<void>;
|
|
58
|
+
refresh: () => Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const AuthContext = createContext<AuthContextValue | null>(null);
|
|
62
|
+
|
|
63
|
+
/** Inside G8AuthGate: the session and its actions. Null elsewhere. */
|
|
64
|
+
export function useG8Auth(): AuthContextValue | null {
|
|
65
|
+
return useContext(AuthContext);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Only same-app paths or http(s) URLs are followed; anything else (javascript:, data:) is dropped. */
|
|
69
|
+
function safeUrl(url: string | undefined): string | undefined {
|
|
70
|
+
if (!url) return undefined;
|
|
71
|
+
if (url.startsWith("/") && !url.startsWith("//")) return url;
|
|
72
|
+
return /^https?:\/\//i.test(url) ? url : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function Frame({ children, labelledBy }: { children: ReactNode; labelledBy?: string }) {
|
|
76
|
+
return (
|
|
77
|
+
<main aria-labelledby={labelledBy} className="flex min-h-dvh items-center justify-center bg-background px-4 py-12">
|
|
78
|
+
<div className="w-full max-w-sm">{children}</div>
|
|
79
|
+
</main>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function BrandMark() {
|
|
84
|
+
const brand = useBrand();
|
|
85
|
+
return (
|
|
86
|
+
<div className="flex items-center gap-2.5">
|
|
87
|
+
{brand.logo ? (
|
|
88
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
89
|
+
<img src={brand.logo} alt="" className="size-8 rounded-md object-contain" />
|
|
90
|
+
) : (
|
|
91
|
+
<span aria-hidden className="flex size-8 items-center justify-center rounded-md bg-primary text-sm font-medium text-primary-foreground">
|
|
92
|
+
{brand.name.trim().charAt(0).toUpperCase() || "A"}
|
|
93
|
+
</span>
|
|
94
|
+
)}
|
|
95
|
+
<span className="truncate text-sm font-medium text-foreground">{brand.name}</span>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function ErrorLine({ id, children }: { id?: string; children: ReactNode }) {
|
|
101
|
+
return (
|
|
102
|
+
<p id={id} role="alert" className="flex items-start gap-1.5 text-xs leading-5 text-error-foreground">
|
|
103
|
+
<AlertTriangle aria-hidden className="mt-0.5 size-3.5 shrink-0" strokeWidth={1.5} />
|
|
104
|
+
<span>{children}</span>
|
|
105
|
+
</p>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function PasswordForm({ onSignedIn }: { onSignedIn: (s: AuthStatus) => void }) {
|
|
110
|
+
const [password, setPassword] = useState("");
|
|
111
|
+
const [show, setShow] = useState(false);
|
|
112
|
+
const [busy, setBusy] = useState(false);
|
|
113
|
+
const [error, setError] = useState<string | null>(null);
|
|
114
|
+
const id = useId();
|
|
115
|
+
|
|
116
|
+
const submit = async (e: FormEvent) => {
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
if (!password || busy) return;
|
|
119
|
+
setBusy(true);
|
|
120
|
+
setError(null);
|
|
121
|
+
try {
|
|
122
|
+
const res = await fetch(AUTH_PATH, { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" }, body: JSON.stringify({ password }) });
|
|
123
|
+
const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
|
124
|
+
setPassword("");
|
|
125
|
+
if (res.ok && body.signedIn === true) onSignedIn(body as unknown as AuthStatus);
|
|
126
|
+
else setError(typeof body.message === "string" ? body.message : `Could not sign in (HTTP ${res.status}).`);
|
|
127
|
+
} catch {
|
|
128
|
+
setError("Could not reach this app's server. Check your connection and try again.");
|
|
129
|
+
} finally {
|
|
130
|
+
setBusy(false);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<form onSubmit={submit} className="flex flex-col gap-3" noValidate>
|
|
136
|
+
<div className="flex flex-col gap-1.5">
|
|
137
|
+
<label htmlFor={`${id}-pw`} className="text-sm font-medium text-foreground">
|
|
138
|
+
Password
|
|
139
|
+
</label>
|
|
140
|
+
<div className="relative">
|
|
141
|
+
<Input
|
|
142
|
+
id={`${id}-pw`}
|
|
143
|
+
type={show ? "text" : "password"}
|
|
144
|
+
autoComplete="current-password"
|
|
145
|
+
autoFocus
|
|
146
|
+
value={password}
|
|
147
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
148
|
+
className="pr-10"
|
|
149
|
+
aria-invalid={error ? true : undefined}
|
|
150
|
+
aria-describedby={error ? `${id}-err` : undefined}
|
|
151
|
+
/>
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
onClick={() => setShow((v) => !v)}
|
|
155
|
+
aria-label={show ? "Hide password" : "Show password"}
|
|
156
|
+
aria-pressed={show}
|
|
157
|
+
className="absolute inset-y-0 right-0 flex w-10 items-center justify-center rounded-r-md text-text-color-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
158
|
+
>
|
|
159
|
+
{show ? <EyeOff aria-hidden className="size-4" strokeWidth={1.5} /> : <Eye aria-hidden className="size-4" strokeWidth={1.5} />}
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
{error ? <ErrorLine id={`${id}-err`}>{error}</ErrorLine> : null}
|
|
164
|
+
<Button type="submit" className="w-full" disabled={!password || busy}>
|
|
165
|
+
{busy ? <Loader2 aria-hidden className="size-4 animate-spin" /> : <ArrowRight aria-hidden className="size-4" strokeWidth={1.5} />}
|
|
166
|
+
{busy ? "Signing in" : "Sign in"}
|
|
167
|
+
</Button>
|
|
168
|
+
</form>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface SignInPanelProps {
|
|
173
|
+
/** Status from a previous GET (the gate passes it); fetched on mount when absent. */
|
|
174
|
+
status?: AuthStatus | null;
|
|
175
|
+
/** Called after a successful sign-in. */
|
|
176
|
+
onSignedIn?: (status: AuthStatus) => void;
|
|
177
|
+
className?: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The sign-in screen: a password form, a "Continue to sign in" link to your provider, or what the server lacks. */
|
|
181
|
+
export function SignInPanel({ status: initial, onSignedIn, className }: SignInPanelProps) {
|
|
182
|
+
const [status, setStatus] = useState<AuthStatus | null | undefined>(initial);
|
|
183
|
+
const brand = useBrand();
|
|
184
|
+
const titleId = useId();
|
|
185
|
+
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
if (initial === undefined) void fetchAuthStatus().then((s) => setStatus(s === "error" ? null : s));
|
|
188
|
+
}, [initial]);
|
|
189
|
+
|
|
190
|
+
const signInUrl = safeUrl(status?.signInUrl);
|
|
191
|
+
const problem = status?.problem;
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<section aria-labelledby={titleId} className={cn("flex flex-col gap-6", className)}>
|
|
195
|
+
<BrandMark />
|
|
196
|
+
<div className="flex flex-col gap-1.5">
|
|
197
|
+
<div aria-hidden className="mb-1 flex size-10 items-center justify-center rounded-lg border border-border bg-muted text-foreground">
|
|
198
|
+
{problem ? <AlertTriangle className="size-5" strokeWidth={1.5} /> : <LockKeyhole className="size-5" strokeWidth={1.5} />}
|
|
199
|
+
</div>
|
|
200
|
+
<h1 id={titleId} className="text-lg font-medium text-foreground">
|
|
201
|
+
{problem ? "Sign-in is not set up yet" : `Sign in to ${brand.name}`}
|
|
202
|
+
</h1>
|
|
203
|
+
<p className="text-sm leading-5 text-text-color-muted">
|
|
204
|
+
{problem
|
|
205
|
+
? "This app asks people to sign in before it shows any data, but its server is missing a setting."
|
|
206
|
+
: status?.kind === "password"
|
|
207
|
+
? "Enter the team password to continue."
|
|
208
|
+
: "Use your account to continue."}
|
|
209
|
+
</p>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
{status === undefined ? (
|
|
213
|
+
<div className="flex h-10 items-center gap-2 text-sm text-text-color-muted" role="status">
|
|
214
|
+
<Loader2 aria-hidden className="size-4 animate-spin" />
|
|
215
|
+
Checking your session
|
|
216
|
+
</div>
|
|
217
|
+
) : problem ? (
|
|
218
|
+
<div className="rounded-lg border border-border bg-muted/40 p-3 text-xs leading-5 text-foreground">
|
|
219
|
+
<p>{problem}</p>
|
|
220
|
+
<p className="mt-2 text-text-color-muted">The settings go in the server environment (.env.local while developing), never in NEXT_PUBLIC_ variables.</p>
|
|
221
|
+
</div>
|
|
222
|
+
) : status?.kind === "password" ? (
|
|
223
|
+
<PasswordForm
|
|
224
|
+
onSignedIn={(s) => {
|
|
225
|
+
setStatus(s);
|
|
226
|
+
onSignedIn?.(s);
|
|
227
|
+
}}
|
|
228
|
+
/>
|
|
229
|
+
) : signInUrl ? (
|
|
230
|
+
<Button asChild className="w-full">
|
|
231
|
+
<a href={signInUrl}>
|
|
232
|
+
<LogIn aria-hidden className="size-4" strokeWidth={1.5} />
|
|
233
|
+
Continue to sign in
|
|
234
|
+
</a>
|
|
235
|
+
</Button>
|
|
236
|
+
) : (
|
|
237
|
+
<p className="rounded-lg border border-border p-3 text-xs leading-5 text-text-color-muted">
|
|
238
|
+
Sign in through your identity provider, then reload this page. To show a sign-in button here, set G8_SIGN_IN_URL in the server environment.
|
|
239
|
+
</p>
|
|
240
|
+
)}
|
|
241
|
+
</section>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export interface G8AuthGateProps {
|
|
246
|
+
children: ReactNode;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Shows SignInPanel instead of the app until the visitor is signed in. Renders the app unchanged when the app has
|
|
251
|
+
* no /api/g8-auth route. The server (the proxy's `authorize`) is the real gate; this only decides what to show.
|
|
252
|
+
*/
|
|
253
|
+
export function G8AuthGate({ children }: G8AuthGateProps) {
|
|
254
|
+
const [status, setStatus] = useState<AuthStatus | null | "error" | undefined>(undefined);
|
|
255
|
+
const [retrying, setRetrying] = useState(false);
|
|
256
|
+
const titleId = useId();
|
|
257
|
+
const latest = useRef(0);
|
|
258
|
+
|
|
259
|
+
const refresh = useCallback(async () => {
|
|
260
|
+
const n = ++latest.current;
|
|
261
|
+
const next = await fetchAuthStatus();
|
|
262
|
+
if (n === latest.current) setStatus(next);
|
|
263
|
+
}, []);
|
|
264
|
+
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
void refresh();
|
|
267
|
+
const onVisible = () => {
|
|
268
|
+
if (document.visibilityState === "visible") void refresh();
|
|
269
|
+
};
|
|
270
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
271
|
+
return () => document.removeEventListener("visibilitychange", onVisible);
|
|
272
|
+
}, [refresh]);
|
|
273
|
+
|
|
274
|
+
const signOut = useCallback(async () => {
|
|
275
|
+
const current = typeof status === "object" ? status : null;
|
|
276
|
+
const out = safeUrl(current?.signOutUrl);
|
|
277
|
+
if (current?.kind !== "password") {
|
|
278
|
+
if (out) window.location.assign(out);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
await fetch(AUTH_PATH, { method: "DELETE", credentials: "same-origin" }).catch(() => undefined);
|
|
282
|
+
latest.current++;
|
|
283
|
+
setStatus({ ...current, signedIn: false });
|
|
284
|
+
}, [status]);
|
|
285
|
+
|
|
286
|
+
if (status === null) return <>{children}</>;
|
|
287
|
+
if (status === undefined) {
|
|
288
|
+
return (
|
|
289
|
+
<Frame>
|
|
290
|
+
<div role="status" className="flex flex-col items-center gap-3 text-sm text-text-color-muted">
|
|
291
|
+
<Loader2 aria-hidden className="size-5 animate-spin" />
|
|
292
|
+
Checking your session
|
|
293
|
+
</div>
|
|
294
|
+
</Frame>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
if (status === "error") {
|
|
298
|
+
return (
|
|
299
|
+
<Frame labelledBy={titleId}>
|
|
300
|
+
<div className="flex flex-col gap-4">
|
|
301
|
+
<BrandMark />
|
|
302
|
+
<h1 id={titleId} className="text-lg font-medium text-foreground">
|
|
303
|
+
Could not check your session
|
|
304
|
+
</h1>
|
|
305
|
+
<p className="text-sm leading-5 text-text-color-muted">This app's server did not answer. Check your connection and try again.</p>
|
|
306
|
+
<Button
|
|
307
|
+
type="button"
|
|
308
|
+
variant="outline"
|
|
309
|
+
className="w-fit"
|
|
310
|
+
disabled={retrying}
|
|
311
|
+
onClick={async () => {
|
|
312
|
+
setRetrying(true);
|
|
313
|
+
await refresh();
|
|
314
|
+
setRetrying(false);
|
|
315
|
+
}}
|
|
316
|
+
>
|
|
317
|
+
{retrying ? <Loader2 aria-hidden className="size-4 animate-spin" /> : <RefreshCw aria-hidden className="size-4" strokeWidth={1.5} />}
|
|
318
|
+
Try again
|
|
319
|
+
</Button>
|
|
320
|
+
</div>
|
|
321
|
+
</Frame>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
if (!status.signedIn) {
|
|
325
|
+
return (
|
|
326
|
+
<Frame>
|
|
327
|
+
<SignInPanel
|
|
328
|
+
status={status}
|
|
329
|
+
onSignedIn={(s) => {
|
|
330
|
+
latest.current++;
|
|
331
|
+
setStatus(s);
|
|
332
|
+
}}
|
|
333
|
+
/>
|
|
334
|
+
</Frame>
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
return <AuthContext.Provider value={{ status, signOut, refresh }}>{children}</AuthContext.Provider>;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Sign out, for the AppShell header. Nothing outside a signed-in G8AuthGate, or when sign-out lives at your provider without a G8_SIGN_OUT_URL. */
|
|
341
|
+
export function G8SignOutButton({ className }: { className?: string }) {
|
|
342
|
+
const auth = useG8Auth();
|
|
343
|
+
const [busy, setBusy] = useState(false);
|
|
344
|
+
if (!auth || !auth.status.signedIn) return null;
|
|
345
|
+
if (auth.status.kind !== "password" && !safeUrl(auth.status.signOutUrl)) return null;
|
|
346
|
+
return (
|
|
347
|
+
<Button
|
|
348
|
+
type="button"
|
|
349
|
+
variant="ghost"
|
|
350
|
+
size="sm"
|
|
351
|
+
className={className}
|
|
352
|
+
disabled={busy}
|
|
353
|
+
onClick={async () => {
|
|
354
|
+
setBusy(true);
|
|
355
|
+
await auth.signOut();
|
|
356
|
+
setBusy(false);
|
|
357
|
+
}}
|
|
358
|
+
>
|
|
359
|
+
{busy ? <Loader2 aria-hidden className="size-4 animate-spin" /> : <LogOut aria-hidden className="size-4" strokeWidth={1.5} />}
|
|
360
|
+
Sign out
|
|
361
|
+
</Button>
|
|
362
|
+
);
|
|
363
|
+
}
|