@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.
Files changed (279) hide show
  1. package/README.md +3 -3
  2. package/blocks/_foundation/README.md +47 -0
  3. package/blocks/_foundation/block.json +13 -0
  4. package/blocks/_foundation/components/auth-gate.tsx +363 -0
  5. package/blocks/_foundation/server/auth.ts +815 -0
  6. package/blocks/campaigns/README.md +1 -1
  7. package/blocks/campaigns/block.json +34 -1
  8. package/blocks/campaigns/components/campaign-links.tsx +180 -0
  9. package/blocks/company-intelligence/components/research-run.tsx +6 -2
  10. package/blocks/crm/README.md +19 -0
  11. package/blocks/crm/block.json +205 -6
  12. package/blocks/crm/components/bulk-update.tsx +119 -0
  13. package/blocks/crm/components/contact-documents-tab.tsx +144 -0
  14. package/blocks/crm/components/contact-insights-tab.tsx +118 -0
  15. package/blocks/crm/components/contact-lookup-link.tsx +47 -6
  16. package/blocks/crm/components/contact-row-actions.tsx +43 -0
  17. package/blocks/crm/components/contact-tag-chips.tsx +113 -0
  18. package/blocks/crm/components/contacts-screen.tsx +46 -5
  19. package/blocks/crm/components/owner-assign.tsx +132 -0
  20. package/blocks/crm/components/record-open-links.tsx +79 -0
  21. package/blocks/crm/components/tag-assign.tsx +112 -0
  22. package/blocks/crm/components/tag-manager.tsx +178 -0
  23. package/blocks/crm/components/unlock-info-button.tsx +82 -0
  24. package/blocks/crm/components/verify-email-button.tsx +93 -0
  25. package/blocks/crm/hooks/use-contact-tags.ts +29 -0
  26. package/blocks/crm/lib/record-actions.ts +102 -0
  27. package/blocks/deals/README.md +1 -0
  28. package/blocks/deals/block.json +17 -0
  29. package/blocks/deals/components/create-deal-button.tsx +109 -0
  30. package/blocks/deals/components/deal-detail.tsx +5 -3
  31. package/blocks/dialer/components/call-log-screen.tsx +6 -1
  32. package/blocks/documents/README.md +86 -0
  33. package/blocks/documents/block.json +378 -0
  34. package/blocks/documents/components/account-bar.tsx +48 -0
  35. package/blocks/documents/components/correspondence-form.tsx +95 -0
  36. package/blocks/documents/components/correspondence-import.tsx +106 -0
  37. package/blocks/documents/components/correspondence-panel.tsx +191 -0
  38. package/blocks/documents/components/documents-screen.tsx +76 -0
  39. package/blocks/documents/components/library-document-detail.tsx +175 -0
  40. package/blocks/documents/components/library-document-form.tsx +105 -0
  41. package/blocks/documents/components/library-folder-form.tsx +74 -0
  42. package/blocks/documents/components/library-panel.tsx +235 -0
  43. package/blocks/documents/components/library-upload.tsx +160 -0
  44. package/blocks/documents/components/question-row.tsx +130 -0
  45. package/blocks/documents/components/questionnaire-detail.tsx +197 -0
  46. package/blocks/documents/components/questionnaires-panel.tsx +173 -0
  47. package/blocks/documents/components/record-documents-tab.tsx +109 -0
  48. package/blocks/documents/lib/documents-types.ts +252 -0
  49. package/blocks/documents/lib/download.ts +64 -0
  50. package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
  51. package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
  52. package/blocks/documents/recipes/organize-the-library.ts +25 -0
  53. package/blocks/email-signatures/README.md +123 -0
  54. package/blocks/email-signatures/block.json +341 -0
  55. package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
  56. package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
  57. package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
  58. package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
  59. package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
  60. package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
  61. package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
  62. package/blocks/email-signatures/components/signature-policy.tsx +130 -0
  63. package/blocks/email-signatures/components/signature-preview.tsx +119 -0
  64. package/blocks/email-signatures/components/signature-report.tsx +77 -0
  65. package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
  66. package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
  67. package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
  68. package/blocks/email-signatures/lib/signatures-types.ts +392 -0
  69. package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
  70. package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
  71. package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
  72. package/blocks/enrichment/README.md +22 -4
  73. package/blocks/enrichment/block.json +145 -5
  74. package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
  75. package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
  76. package/blocks/enrichment/components/bulk-enrich-panel.tsx +7 -7
  77. package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
  78. package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
  79. package/blocks/enrichment/components/enrichment-screen.tsx +34 -3
  80. package/blocks/enrichment/components/list-pipelines.tsx +97 -0
  81. package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
  82. package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
  83. package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
  84. package/blocks/enrichment/lib/enrichment-types.ts +12 -0
  85. package/blocks/enrichment/lib/pipeline-types.ts +234 -0
  86. package/blocks/forms/README.md +2 -0
  87. package/blocks/forms/components/forms-screen.tsx +6 -3
  88. package/blocks/inbox/README.md +40 -4
  89. package/blocks/inbox/block.json +130 -8
  90. package/blocks/inbox/components/blocked-senders.tsx +103 -0
  91. package/blocks/inbox/components/compose-message.tsx +193 -0
  92. package/blocks/inbox/components/inbox-screen.tsx +54 -6
  93. package/blocks/inbox/components/inbox-search.tsx +106 -0
  94. package/blocks/inbox/components/tag-manager.tsx +136 -0
  95. package/blocks/inbox/components/thread-actions.tsx +133 -0
  96. package/blocks/inbox/components/thread-view.tsx +2 -0
  97. package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
  98. package/blocks/inbox/lib/inbox-manage.ts +227 -0
  99. package/blocks/newsletter/components/newsletter-screen.tsx +3 -1
  100. package/blocks/pages/block.json +10 -1
  101. package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
  102. package/blocks/programmatic-pages/README.md +82 -0
  103. package/blocks/programmatic-pages/block.json +292 -0
  104. package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
  105. package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
  106. package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
  107. package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
  108. package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
  109. package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
  110. package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
  111. package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
  112. package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
  113. package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
  114. package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
  115. package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
  116. package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
  117. package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
  118. package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
  119. package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
  120. package/blocks/quotes/README.md +25 -1
  121. package/blocks/quotes/block.json +123 -6
  122. package/blocks/quotes/components/manual-products.tsx +144 -0
  123. package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
  124. package/blocks/quotes/components/quote-documents.tsx +94 -0
  125. package/blocks/quotes/components/quote-evidence.tsx +95 -0
  126. package/blocks/quotes/components/quote-list.tsx +23 -5
  127. package/blocks/quotes/components/quote-settings.tsx +184 -0
  128. package/blocks/quotes/components/quote-templates.tsx +189 -0
  129. package/blocks/quotes/components/quote-viewer.tsx +16 -3
  130. package/blocks/quotes/components/quotes-screen.tsx +15 -5
  131. package/blocks/quotes/lib/quote-admin.ts +264 -0
  132. package/blocks/sequencer/README.md +55 -0
  133. package/blocks/sequencer/block.json +172 -5
  134. package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
  135. package/blocks/sequencer/components/sequence-builder.tsx +13 -3
  136. package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
  137. package/blocks/sequencer/components/sequence-detail.tsx +6 -0
  138. package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
  139. package/blocks/sequencer/components/sequence-enrollments.tsx +2 -1
  140. package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
  141. package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
  142. package/blocks/sequencer/components/sequence-templates.tsx +160 -0
  143. package/blocks/sequencer/components/sequences-screen.tsx +36 -6
  144. package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
  145. package/blocks/sequencer/components/step-editor.tsx +10 -1
  146. package/blocks/sequencer/components/step-templates.tsx +224 -0
  147. package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
  148. package/blocks/sequencer/lib/sequencer-utils.ts +4 -0
  149. package/blocks/signals/README.md +39 -0
  150. package/blocks/signals/block.json +287 -0
  151. package/blocks/signals/components/auto-seed-panel.tsx +111 -0
  152. package/blocks/signals/components/listener-form.tsx +184 -0
  153. package/blocks/signals/components/matched-posts.tsx +195 -0
  154. package/blocks/signals/components/post-engagements.tsx +114 -0
  155. package/blocks/signals/components/record-social-tab.tsx +49 -0
  156. package/blocks/signals/components/social-listeners-screen.tsx +250 -0
  157. package/blocks/signals/components/social-settings.tsx +128 -0
  158. package/blocks/signals/components/source-lists.tsx +83 -0
  159. package/blocks/signals/components/top-engagers.tsx +68 -0
  160. package/blocks/signals/components/x-integration.tsx +129 -0
  161. package/blocks/signals/lib/signals-types.ts +228 -0
  162. package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
  163. package/blocks/signals/recipes/spend-against-caps.ts +14 -0
  164. package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
  165. package/blocks/visitors/components/traffic-chart.tsx +3 -2
  166. package/blocks/web-analytics/README.md +102 -0
  167. package/blocks/web-analytics/block.json +292 -0
  168. package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
  169. package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
  170. package/blocks/web-analytics/components/audience-tab.tsx +135 -0
  171. package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
  172. package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
  173. package/blocks/web-analytics/components/overview-tab.tsx +102 -0
  174. package/blocks/web-analytics/components/pages-tab.tsx +148 -0
  175. package/blocks/web-analytics/components/ranked-table.tsx +103 -0
  176. package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
  177. package/blocks/web-analytics/components/report-card.tsx +71 -0
  178. package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
  179. package/blocks/web-analytics/hooks/use-report.ts +48 -0
  180. package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
  181. package/blocks/web-analytics/lib/page-path.ts +14 -0
  182. package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
  183. package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
  184. package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
  185. package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
  186. package/blocks/workbench/README.md +54 -2
  187. package/blocks/workbench/block.json +234 -5
  188. package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
  189. package/blocks/workbench/components/workbench-automation.tsx +36 -0
  190. package/blocks/workbench/components/workbench-connections.tsx +136 -0
  191. package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
  192. package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
  193. package/blocks/workbench/components/workbench-grid.tsx +70 -7
  194. package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
  195. package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
  196. package/blocks/workbench/components/workbench-schedule.tsx +191 -0
  197. package/blocks/workbench/components/workbench-screen.tsx +50 -17
  198. package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
  199. package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
  200. package/blocks/workbench/components/workbench-triggers.tsx +178 -0
  201. package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
  202. package/blocks/workbench/lib/api-status.ts +8 -0
  203. package/blocks/workbench/lib/automation-types.ts +257 -0
  204. package/dist/cli.js +257 -51
  205. package/dist/index.browser.js +1 -1
  206. package/dist/index.browser.js.map +1 -1
  207. package/dist/index.browser.mjs +1 -1
  208. package/dist/index.browser.mjs.map +1 -1
  209. package/dist/index.js +1 -1
  210. package/dist/index.js.map +1 -1
  211. package/dist/index.mjs +1 -1
  212. package/dist/index.mjs.map +1 -1
  213. package/dist/react.browser.js +1 -1
  214. package/dist/react.browser.js.map +1 -1
  215. package/dist/react.browser.mjs +1 -1
  216. package/dist/react.browser.mjs.map +1 -1
  217. package/dist/react.js +1 -1
  218. package/dist/react.js.map +1 -1
  219. package/dist/react.mjs +1 -1
  220. package/dist/react.mjs.map +1 -1
  221. package/dist/types/blocks/auth.d.ts +45 -0
  222. package/dist/types/blocks/create.d.ts +5 -2
  223. package/dist/types/version.d.ts +2 -2
  224. package/docs/api/forms.md +1 -1
  225. package/package.json +1 -1
  226. package/stacks/6sense/pages/contacts.tsx.tmpl +2 -0
  227. package/stacks/6sense.json +20 -0
  228. package/stacks/apollo/pages/calls.tsx.tmpl +9 -9
  229. package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
  230. package/stacks/apollo/pages/contacts.tsx.tmpl +6 -0
  231. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +3 -3
  232. package/stacks/apollo/pages/lists.tsx.tmpl +14 -2
  233. package/stacks/apollo/pages/sequences.tsx.tmpl +8 -2
  234. package/stacks/apollo.json +33 -2
  235. package/stacks/clay/pages/contacts.tsx.tmpl +2 -0
  236. package/stacks/clay/pages/lists.tsx.tmpl +14 -2
  237. package/stacks/gong/pages/calls.tsx.tmpl +9 -9
  238. package/stacks/gong/pages/contacts.tsx.tmpl +2 -0
  239. package/stacks/hootsuite/pages/contacts.tsx.tmpl +2 -0
  240. package/stacks/hootsuite.json +10 -0
  241. package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
  242. package/stacks/hubspot/pages/companies.tsx.tmpl +2 -0
  243. package/stacks/hubspot/pages/contacts.tsx.tmpl +6 -0
  244. package/stacks/hubspot/pages/deals.tsx.tmpl +4 -0
  245. package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
  246. package/stacks/hubspot/pages/inbox.tsx.tmpl +2 -2
  247. package/stacks/hubspot/pages/sequences.tsx.tmpl +8 -2
  248. package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
  249. package/stacks/hubspot.json +45 -2
  250. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +3 -3
  251. package/stacks/intercom.json +10 -0
  252. package/stacks/mailchimp/pages/contacts.tsx.tmpl +2 -0
  253. package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
  254. package/stacks/mailchimp.json +2 -1
  255. package/stacks/outreach/pages/calls.tsx.tmpl +9 -9
  256. package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
  257. package/stacks/outreach/pages/contacts.tsx.tmpl +2 -0
  258. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +3 -3
  259. package/stacks/outreach/pages/sequences.tsx.tmpl +8 -2
  260. package/stacks/outreach.json +13 -2
  261. package/stacks/pipedrive/pages/contacts.tsx.tmpl +2 -0
  262. package/stacks/pipedrive/pages/inbox.tsx.tmpl +2 -2
  263. package/stacks/pipedrive.json +10 -0
  264. package/stacks/salesforce/pages/companies.tsx.tmpl +2 -0
  265. package/stacks/salesforce/pages/contacts.tsx.tmpl +6 -0
  266. package/stacks/salesforce/pages/deals.tsx.tmpl +4 -0
  267. package/stacks/salesforce.json +10 -0
  268. package/stacks/zoominfo/pages/contacts.tsx.tmpl +4 -0
  269. package/stacks/zoominfo/pages/lists.tsx.tmpl +14 -2
  270. package/stacks/zoominfo.json +20 -0
  271. package/styles/styles.css +1 -1
  272. package/templates/auth/auth-route.ts.tmpl +16 -0
  273. package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
  274. package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
  275. package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
  276. package/templates/auth/g8.auth.password.ts.tmpl +18 -0
  277. package/templates/auth/g8.auth.session.ts.tmpl +23 -0
  278. package/templates/auth/middleware.ts.tmpl +14 -0
  279. 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 mailboxes; 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, 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&apos;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
+ }