@connextar/house 0.1.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 +180 -0
- package/dist/account/data-contract.d.ts +105 -0
- package/dist/account/data-contract.d.ts.map +1 -0
- package/dist/account/data-contract.js +46 -0
- package/dist/account/data-contract.js.map +1 -0
- package/dist/account/index.d.ts +2 -0
- package/dist/account/index.d.ts.map +1 -0
- package/dist/account/index.js +2 -0
- package/dist/account/index.js.map +1 -0
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +2 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/two-factor-policy.d.ts +49 -0
- package/dist/auth/two-factor-policy.d.ts.map +1 -0
- package/dist/auth/two-factor-policy.js +58 -0
- package/dist/auth/two-factor-policy.js.map +1 -0
- package/dist/errors/error-shell.d.ts +40 -0
- package/dist/errors/error-shell.d.ts.map +1 -0
- package/dist/errors/error-shell.js +41 -0
- package/dist/errors/error-shell.js.map +1 -0
- package/dist/errors/global-error-shell.d.ts +41 -0
- package/dist/errors/global-error-shell.d.ts.map +1 -0
- package/dist/errors/global-error-shell.js +39 -0
- package/dist/errors/global-error-shell.js.map +1 -0
- package/dist/errors/index.d.ts +3 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +3 -0
- package/dist/errors/index.js.map +1 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# @connextar/house
|
|
2
|
+
|
|
3
|
+
The pieces every app we build needs and none of them should own a copy of.
|
|
4
|
+
|
|
5
|
+
Thirteen codebases grew from one shared way of building, with no shared code. This
|
|
6
|
+
is the dependency that stops the same fix having to be made eleven times — but
|
|
7
|
+
only for the things that are genuinely the same everywhere. It is deliberately
|
|
8
|
+
small, and the reason it is small is worth reading before adding to it.
|
|
9
|
+
|
|
10
|
+
## What is in it, and what is not
|
|
11
|
+
|
|
12
|
+
The portfolio was measured before anything was extracted. Divergence, against
|
|
13
|
+
Quickie, of files that exist in six or more apps:
|
|
14
|
+
|
|
15
|
+
| File | copies | divergence |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `error-shell.tsx` | 6/6 | **0%** |
|
|
18
|
+
| `check-motion.mjs` | 3/6 | 11% |
|
|
19
|
+
| `e2e-local.sh` | 6/6 | 30% |
|
|
20
|
+
| `button.tsx` | 6/6 | 42% |
|
|
21
|
+
| `dialog.tsx` | 5/6 | 62% |
|
|
22
|
+
| `card.tsx` | 5/6 | 106% |
|
|
23
|
+
| `lib/api.ts` | 6/6 | 152% |
|
|
24
|
+
| `auth.ts` | 4/6 | 42–103% |
|
|
25
|
+
|
|
26
|
+
Everything below about 15% is an extraction. Everything above it is a rewrite
|
|
27
|
+
wearing an extraction's clothes, and pulling it in here would mean thirteen apps
|
|
28
|
+
adopting a component none of them currently has. So the rule is:
|
|
29
|
+
|
|
30
|
+
> **A shared package pays for itself on code that has not diverged yet.**
|
|
31
|
+
> Extract what is identical, and what you are about to write for the second time.
|
|
32
|
+
> Leave the rest where it is and let `bev`'s drift report keep score.
|
|
33
|
+
|
|
34
|
+
That is why there is no `Button` here, and why the error shell takes its actions
|
|
35
|
+
as props instead of rendering its own. Bringing a second design system into an
|
|
36
|
+
app that already has one is a cost with no matching benefit.
|
|
37
|
+
|
|
38
|
+
## Modules
|
|
39
|
+
|
|
40
|
+
### `@connextar/house/errors`
|
|
41
|
+
|
|
42
|
+
`ErrorShell` — the frame behind `error.tsx` and `not-found.tsx`. Renders plain
|
|
43
|
+
elements with the house token classes (`bg-card`, `border-border`,
|
|
44
|
+
`text-muted-foreground`), so it inherits the host app's look without importing a
|
|
45
|
+
single thing from it. That matters here more than anywhere: it renders precisely
|
|
46
|
+
when something upstream has already failed.
|
|
47
|
+
|
|
48
|
+
`GlobalErrorShell` — the whole of `global-error.tsx`. This one was the most
|
|
49
|
+
duplicated file in the portfolio for a structural reason: a global error boundary
|
|
50
|
+
replaces the root layout, so it cannot import the app's components, which left
|
|
51
|
+
eight apps each carrying sixty lines of near-identical inline CSS whose only real
|
|
52
|
+
difference was five hex values. Those five are now props.
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
// app/global-error.tsx
|
|
56
|
+
"use client";
|
|
57
|
+
import { GlobalErrorShell } from "@connextar/house/errors";
|
|
58
|
+
import "@styles/globals.css";
|
|
59
|
+
|
|
60
|
+
export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
|
|
61
|
+
return (
|
|
62
|
+
<GlobalErrorShell
|
|
63
|
+
appName="Quickie"
|
|
64
|
+
brand={{ background: "#ffffff", ink: "#0f172a", muted: "#5b6b7f", line: "#e2e8f0", accent: "#0575b0" }}
|
|
65
|
+
error={error}
|
|
66
|
+
reset={reset}
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### `@connextar/house/auth`
|
|
73
|
+
|
|
74
|
+
The two-factor sign-in policy. Better Auth's `twoFactor` plugin hooks
|
|
75
|
+
`/sign-in/email`, `/sign-in/username` and `/sign-in/phone-number` and nothing
|
|
76
|
+
else — every other sign-in route issues a session outright, second factor or not.
|
|
77
|
+
On an app where magic link is a first-class sign-in method that makes two-factor
|
|
78
|
+
decorative: a stolen mailbox is still enough.
|
|
79
|
+
|
|
80
|
+
This module decides which routes to refuse, and why. It is here rather than in
|
|
81
|
+
each app because it has to be the same everywhere and because getting it wrong is
|
|
82
|
+
silent — two-factor keeps *looking* enabled while doing nothing.
|
|
83
|
+
|
|
84
|
+
The lookup is injected, because only the app knows its own user table:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { shouldBlockForTwoFactor } from "@connextar/house/auth";
|
|
88
|
+
|
|
89
|
+
const blocked = await shouldBlockForTwoFactor(ctx.path, email, async (address) => {
|
|
90
|
+
const user = await prisma.user.findUnique({ where: { email: address }, select: { twoFactorEnabled: true } });
|
|
91
|
+
return user?.twoFactorEnabled === true;
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Passkey and social sign-in are deliberately still allowed. A passkey is already
|
|
96
|
+
possession of a device and is phishing-resistant; a social login delegates to a
|
|
97
|
+
provider enforcing its own second factor. Neither is weaker than password plus
|
|
98
|
+
TOTP. A link in an inbox is.
|
|
99
|
+
|
|
100
|
+
### `@connextar/house/account`
|
|
101
|
+
|
|
102
|
+
The contract for "your data" and "delete my account" — types, the export notice,
|
|
103
|
+
and the sole-owner refusal message. The *implementation* stays per-app, because
|
|
104
|
+
an export is a walk over that app's Prisma schema. What must not be per-app is
|
|
105
|
+
the shape of the answer, since that is what a person receives and a regulator
|
|
106
|
+
reads.
|
|
107
|
+
|
|
108
|
+
Two decisions are encoded here rather than left to be rediscovered:
|
|
109
|
+
|
|
110
|
+
- **An export summarises authored work rather than including it.** A note
|
|
111
|
+
somebody wrote about a customer is the customer's personal data with an author
|
|
112
|
+
attached; shipping the contents would turn a subject access request into a way
|
|
113
|
+
for one employee to walk out with the customer book.
|
|
114
|
+
- **Deletion follows the schema.** `onDelete: Cascade` is the app already saying
|
|
115
|
+
"personal, goes with them"; `onDelete: SetNull` is "business record, outlives
|
|
116
|
+
them". Invoices have to survive, and an audit trail erasable by its subject was
|
|
117
|
+
never an audit trail.
|
|
118
|
+
|
|
119
|
+
App-specific sections go in `extras`, so one app's furniture does not become
|
|
120
|
+
everyone's obligation.
|
|
121
|
+
|
|
122
|
+
## Publishing
|
|
123
|
+
|
|
124
|
+
The scope is public and the contents are a design system and two policy modules —
|
|
125
|
+
nothing here is a secret. Public publishing is what keeps thirteen repositories
|
|
126
|
+
across two GitHub owners able to install it with no token in any Vercel project,
|
|
127
|
+
which is the failure mode that would otherwise take every deploy down at once.
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
npm login # once, as an owner of the @connextar scope
|
|
131
|
+
npm run verify # format, typecheck, lint, tests, build
|
|
132
|
+
npm publish # publishConfig.access is already "public"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`prepublishOnly` runs `verify`, so a broken build cannot be published.
|
|
136
|
+
|
|
137
|
+
## Adopting it in an app
|
|
138
|
+
|
|
139
|
+
Once published:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npm install @connextar/house
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Then replace the local copies. For Quickie the exact change is saved as a patch,
|
|
146
|
+
generated from the working migration and verified against a real packed tarball:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
git apply ../../Shared/cx-house/migrations/quickie-adopt-house.patch
|
|
150
|
+
npm run check && npm test && npm run build
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
For the other apps the shape is the same: `global-error.tsx` becomes a call to
|
|
154
|
+
`GlobalErrorShell`, `components/errors/error-shell.tsx` re-exports `ErrorShell`
|
|
155
|
+
and keeps only its own `ErrorAction`, and any two-factor policy defers to
|
|
156
|
+
`@connextar/house/auth`.
|
|
157
|
+
|
|
158
|
+
**A note on local development.** Installing this with `npm install ../path` makes
|
|
159
|
+
a symlink, and Next's Turbopack will not resolve a package whose real path is
|
|
160
|
+
outside the app's project root — the build fails with `Module not found` even
|
|
161
|
+
though `node` and `tsc` both resolve it fine. Use a packed tarball instead, which
|
|
162
|
+
behaves exactly like a registry install:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
npm pack --pack-destination /tmp # in this repo
|
|
166
|
+
npm install /tmp/connextar-house-0.1.0.tgz # in the app
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Adding to it
|
|
170
|
+
|
|
171
|
+
Two questions, in order:
|
|
172
|
+
|
|
173
|
+
1. **Is it already identical in three or more apps?** Check `bev`'s
|
|
174
|
+
`npm run check:shared-drift`. If the copies have diverged, extracting is a
|
|
175
|
+
rewrite — say so out loud and decide whether the rewrite is worth it on its
|
|
176
|
+
own merits.
|
|
177
|
+
2. **Does it need anything from the host app?** If yes, inject it as a prop or a
|
|
178
|
+
function argument. This package imports no app's Prisma client, no app's
|
|
179
|
+
design system and no app's routes, and that is what makes it droppable into
|
|
180
|
+
the next one.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What "your data" and "delete my account" mean, as a contract.
|
|
3
|
+
*
|
|
4
|
+
* The *implementation* of both is unavoidably per-app — every app has its own
|
|
5
|
+
* Prisma schema, and an export is a walk over it. What must not be per-app is
|
|
6
|
+
* the shape of the answer, because that is the bit a person receives, a
|
|
7
|
+
* regulator reads, and a second app has to stay consistent with.
|
|
8
|
+
*
|
|
9
|
+
* Two decisions are encoded here rather than left to each app to rediscover:
|
|
10
|
+
*
|
|
11
|
+
* 1. **An export summarises authored work rather than including it.** A note
|
|
12
|
+
* somebody wrote about a customer is the customer's personal data with an
|
|
13
|
+
* author attached. Shipping the contents would turn a subject access request
|
|
14
|
+
* into a way for one employee to walk out with the whole customer book.
|
|
15
|
+
* Counts, dates and the owning organisation instead — enough to know the
|
|
16
|
+
* records exist and to ask for them specifically.
|
|
17
|
+
*
|
|
18
|
+
* 2. **Deletion follows the schema, not a hand-written list.** `onDelete:
|
|
19
|
+
* Cascade` is the app already saying "this is personal and goes with them";
|
|
20
|
+
* `onDelete: SetNull` is it saying "this is a business record that outlives
|
|
21
|
+
* them". Invoices have to survive — HMRC wants six years — and an audit trail
|
|
22
|
+
* erasable by its subject was never an audit trail. Re-deciding that in code
|
|
23
|
+
* is how the two drift apart.
|
|
24
|
+
*/
|
|
25
|
+
/** Bumped when the export shape changes, so an old file stays readable. */
|
|
26
|
+
export declare const ACCOUNT_EXPORT_SCHEMA_VERSION = 1;
|
|
27
|
+
export declare const ACCOUNT_EXPORT_NOTICE: string;
|
|
28
|
+
/** A count of something the person authored. Never the contents. */
|
|
29
|
+
export interface AuthoredSummary {
|
|
30
|
+
/** Human-readable, e.g. "Customer notes". Shown verbatim in the file. */
|
|
31
|
+
type: string;
|
|
32
|
+
count: number;
|
|
33
|
+
earliest: Date | string | null;
|
|
34
|
+
latest: Date | string | null;
|
|
35
|
+
}
|
|
36
|
+
export interface AccountExportSignIn {
|
|
37
|
+
hasPassword: boolean;
|
|
38
|
+
/** Provider ids only. Never tokens — an export is not a credential dump. */
|
|
39
|
+
socialProviders: string[];
|
|
40
|
+
twoFactorEnabled: boolean;
|
|
41
|
+
passkeys: {
|
|
42
|
+
name: string | null;
|
|
43
|
+
deviceType: string;
|
|
44
|
+
createdAt: Date | string;
|
|
45
|
+
}[];
|
|
46
|
+
}
|
|
47
|
+
export interface AccountExport {
|
|
48
|
+
exportedAt: string;
|
|
49
|
+
schemaVersion: typeof ACCOUNT_EXPORT_SCHEMA_VERSION;
|
|
50
|
+
notice: string;
|
|
51
|
+
/** The user row, minus anything secret and minus the preference fields below. */
|
|
52
|
+
profile: Record<string, unknown>;
|
|
53
|
+
organisations: {
|
|
54
|
+
name: string;
|
|
55
|
+
role: string;
|
|
56
|
+
joinedAt: Date | string;
|
|
57
|
+
}[];
|
|
58
|
+
signIn: AccountExportSignIn;
|
|
59
|
+
sessions: {
|
|
60
|
+
createdAt: Date | string;
|
|
61
|
+
expiresAt: Date | string;
|
|
62
|
+
ipAddress: string | null;
|
|
63
|
+
userAgent: string | null;
|
|
64
|
+
}[];
|
|
65
|
+
preferences: Record<string, unknown>;
|
|
66
|
+
authored: AuthoredSummary[];
|
|
67
|
+
/**
|
|
68
|
+
* Sections only this app has — Quickie's saved views, a school's linked
|
|
69
|
+
* pupils. Kept as a named bag rather than widened into the contract, so that
|
|
70
|
+
* what every app owes a person stays legible and one app's furniture does not
|
|
71
|
+
* become everyone's obligation.
|
|
72
|
+
*/
|
|
73
|
+
extras?: Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
export interface DeletionBlocker {
|
|
76
|
+
id: string;
|
|
77
|
+
name: string;
|
|
78
|
+
otherMembers: number;
|
|
79
|
+
}
|
|
80
|
+
export interface DeletionPreflight {
|
|
81
|
+
canDelete: boolean;
|
|
82
|
+
/** Organisations destroyed along with the account — the person is their only member. */
|
|
83
|
+
organisationsToDelete: {
|
|
84
|
+
id: string;
|
|
85
|
+
name: string;
|
|
86
|
+
}[];
|
|
87
|
+
/** Organisations they solely own that still have members. Each one blocks deletion. */
|
|
88
|
+
blockedBy: DeletionBlocker[];
|
|
89
|
+
/** Rows removed outright, by kind. Everything else is orphaned, not deleted. */
|
|
90
|
+
willRemove: {
|
|
91
|
+
type: string;
|
|
92
|
+
count: number;
|
|
93
|
+
}[];
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Why a deletion was refused, in words the person can act on.
|
|
97
|
+
*
|
|
98
|
+
* Shared so that every app phrases the sole-owner case the same way. It is the
|
|
99
|
+
* one refusal people actually hit, and "you can't do that" without saying what
|
|
100
|
+
* to do instead is how a support ticket gets written.
|
|
101
|
+
*/
|
|
102
|
+
export declare function deletionBlockedMessage(blockers: DeletionBlocker[]): string;
|
|
103
|
+
/** True when the person is about to destroy an organisation, not just leave one. */
|
|
104
|
+
export declare function deletionDestroysOrganisations(preflight: DeletionPreflight): boolean;
|
|
105
|
+
//# sourceMappingURL=data-contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-contract.d.ts","sourceRoot":"","sources":["../../src/account/data-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,2EAA2E;AAC3E,eAAO,MAAM,6BAA6B,IAAI,CAAC;AAE/C,eAAO,MAAM,qBAAqB,QAG+B,CAAC;AAElE,oEAAoE;AACpE,MAAM,WAAW,eAAe;IAC9B,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAC;IAC/B,MAAM,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,OAAO,CAAC;IACrB,4EAA4E;IAC5E,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,IAAI,GAAG,MAAM,CAAA;KAAE,EAAE,CAAC;CACnF;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,OAAO,6BAA6B,CAAC;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,aAAa,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,IAAI,GAAG,MAAM,CAAA;KAAE,EAAE,CAAC;IACzE,MAAM,EAAE,mBAAmB,CAAC;IAC5B,QAAQ,EAAE;QACR,SAAS,EAAE,IAAI,GAAG,MAAM,CAAC;QACzB,SAAS,EAAE,IAAI,GAAG,MAAM,CAAC;QACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;KAC1B,EAAE,CAAC;IACJ,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAID,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,OAAO,CAAC;IACnB,wFAAwF;IACxF,qBAAqB,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACtD,uFAAuF;IACvF,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,gFAAgF;IAChF,UAAU,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC/C;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,MAAM,CAM1E;AAED,oFAAoF;AACpF,wBAAgB,6BAA6B,CAAC,SAAS,EAAE,iBAAiB,GAAG,OAAO,CAEnF"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What "your data" and "delete my account" mean, as a contract.
|
|
3
|
+
*
|
|
4
|
+
* The *implementation* of both is unavoidably per-app — every app has its own
|
|
5
|
+
* Prisma schema, and an export is a walk over it. What must not be per-app is
|
|
6
|
+
* the shape of the answer, because that is the bit a person receives, a
|
|
7
|
+
* regulator reads, and a second app has to stay consistent with.
|
|
8
|
+
*
|
|
9
|
+
* Two decisions are encoded here rather than left to each app to rediscover:
|
|
10
|
+
*
|
|
11
|
+
* 1. **An export summarises authored work rather than including it.** A note
|
|
12
|
+
* somebody wrote about a customer is the customer's personal data with an
|
|
13
|
+
* author attached. Shipping the contents would turn a subject access request
|
|
14
|
+
* into a way for one employee to walk out with the whole customer book.
|
|
15
|
+
* Counts, dates and the owning organisation instead — enough to know the
|
|
16
|
+
* records exist and to ask for them specifically.
|
|
17
|
+
*
|
|
18
|
+
* 2. **Deletion follows the schema, not a hand-written list.** `onDelete:
|
|
19
|
+
* Cascade` is the app already saying "this is personal and goes with them";
|
|
20
|
+
* `onDelete: SetNull` is it saying "this is a business record that outlives
|
|
21
|
+
* them". Invoices have to survive — HMRC wants six years — and an audit trail
|
|
22
|
+
* erasable by its subject was never an audit trail. Re-deciding that in code
|
|
23
|
+
* is how the two drift apart.
|
|
24
|
+
*/
|
|
25
|
+
/** Bumped when the export shape changes, so an old file stays readable. */
|
|
26
|
+
export const ACCOUNT_EXPORT_SCHEMA_VERSION = 1;
|
|
27
|
+
export const ACCOUNT_EXPORT_NOTICE = "This file contains the personal data we hold about you. Work you authored — notes, interactions, " +
|
|
28
|
+
"expenses — belongs to the organisation it was created in and is summarised rather than included. " +
|
|
29
|
+
"Ask an owner of that organisation for the records themselves.";
|
|
30
|
+
/**
|
|
31
|
+
* Why a deletion was refused, in words the person can act on.
|
|
32
|
+
*
|
|
33
|
+
* Shared so that every app phrases the sole-owner case the same way. It is the
|
|
34
|
+
* one refusal people actually hit, and "you can't do that" without saying what
|
|
35
|
+
* to do instead is how a support ticket gets written.
|
|
36
|
+
*/
|
|
37
|
+
export function deletionBlockedMessage(blockers) {
|
|
38
|
+
const names = blockers.map((b) => b.name).join(", ");
|
|
39
|
+
return (`You are the only owner of ${names}. Make someone else an owner, or remove the other members, ` +
|
|
40
|
+
"before deleting your account.");
|
|
41
|
+
}
|
|
42
|
+
/** True when the person is about to destroy an organisation, not just leave one. */
|
|
43
|
+
export function deletionDestroysOrganisations(preflight) {
|
|
44
|
+
return preflight.organisationsToDelete.length > 0;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=data-contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-contract.js","sourceRoot":"","sources":["../../src/account/data-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,2EAA2E;AAC3E,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,CAAC;AAE/C,MAAM,CAAC,MAAM,qBAAqB,GAChC,mGAAmG;IACnG,mGAAmG;IACnG,+DAA+D,CAAC;AA8DlE;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CAAC,QAA2B;IAChE,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrD,OAAO,CACL,6BAA6B,KAAK,6DAA6D;QAC/F,+BAA+B,CAChC,CAAC;AACJ,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,6BAA6B,CAAC,SAA4B;IACxE,OAAO,SAAS,CAAC,qBAAqB,CAAC,MAAM,GAAG,CAAC,CAAC;AACpD,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { ACCOUNT_EXPORT_NOTICE, ACCOUNT_EXPORT_SCHEMA_VERSION, deletionBlockedMessage, deletionDestroysOrganisations, type AccountExport, type AccountExportSignIn, type AuthoredSummary, type DeletionBlocker, type DeletionPreflight, } from "./data-contract";
|
|
2
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/account/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,6BAA6B,EAC7B,sBAAsB,EACtB,6BAA6B,EAC7B,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,iBAAiB,GACvB,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/account/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,6BAA6B,EAC7B,sBAAsB,EACtB,6BAA6B,GAM9B,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,EAC1B,wBAAwB,EACxB,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,EACjB,KAAK,oBAAoB,EACzB,KAAK,eAAe,GACrB,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,EAC1B,wBAAwB,EACxB,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,GAGlB,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which sign-in routes need the second factor enforced by hand.
|
|
3
|
+
*
|
|
4
|
+
* Better Auth's `twoFactor` plugin hooks `/sign-in/email`, `/sign-in/username`
|
|
5
|
+
* and `/sign-in/phone-number` and nothing else. Every other sign-in route issues
|
|
6
|
+
* a session outright, second factor or not. That is fine for most of them and
|
|
7
|
+
* emphatically not fine for one:
|
|
8
|
+
*
|
|
9
|
+
* - **Magic link** — a link in an inbox, and strictly weaker than the password
|
|
10
|
+
* it stands in for. Allowing it while two-factor is on means a stolen mailbox
|
|
11
|
+
* is still enough, which is exactly what the feature is sold as preventing.
|
|
12
|
+
* **Refused.**
|
|
13
|
+
* - **Passkey** — already possession of a device, and phishing-resistant. At
|
|
14
|
+
* least as strong as password + TOTP. Allowed.
|
|
15
|
+
* - **Social** — delegates to a provider that enforces its own second factor.
|
|
16
|
+
* Allowed.
|
|
17
|
+
*
|
|
18
|
+
* This lives in the package rather than in each app because it is a security
|
|
19
|
+
* decision that has to be the same everywhere, and because getting it wrong is
|
|
20
|
+
* silent: two-factor keeps *looking* enabled while doing nothing.
|
|
21
|
+
*/
|
|
22
|
+
export declare const TWO_FACTOR_BLOCKED_PATHS: readonly ["/sign-in/magic-link"];
|
|
23
|
+
export type TwoFactorBlockedPath = (typeof TWO_FACTOR_BLOCKED_PATHS)[number];
|
|
24
|
+
export declare const TWO_FACTOR_BLOCKED_MESSAGE: string;
|
|
25
|
+
export declare function isTwoFactorBlockedPath(path: string): boolean;
|
|
26
|
+
/** Answers "does this address have a second factor turned on?". Supplied by the app. */
|
|
27
|
+
export type TwoFactorLookup = (email: string) => Promise<boolean>;
|
|
28
|
+
/**
|
|
29
|
+
* True when this sign-in attempt must be refused because it would hand out a
|
|
30
|
+
* session without the second factor.
|
|
31
|
+
*
|
|
32
|
+
* The lookup is injected rather than imported: every app has its own Prisma
|
|
33
|
+
* client and its own user table, and a package that reached for one would be
|
|
34
|
+
* useless to the next app along.
|
|
35
|
+
*
|
|
36
|
+
* Unknown addresses return false. The refusal must not become an oracle
|
|
37
|
+
* answering "does this account exist, and does it have two-factor on?" for
|
|
38
|
+
* anyone with an email address and a spare minute.
|
|
39
|
+
*/
|
|
40
|
+
export declare function shouldBlockForTwoFactor(path: string, email: string | undefined | null, lookup: TwoFactorLookup): Promise<boolean>;
|
|
41
|
+
/**
|
|
42
|
+
* Pull the shared secret out of an `otpauth://` URI, for the "can't scan it?"
|
|
43
|
+
* manual-entry path during enrolment.
|
|
44
|
+
*
|
|
45
|
+
* Returns null rather than throwing on a malformed URI — a missing manual-entry
|
|
46
|
+
* fallback is a worse enrolment screen, not a broken one.
|
|
47
|
+
*/
|
|
48
|
+
export declare function totpSecretFromUri(totpUri: string): string | null;
|
|
49
|
+
//# sourceMappingURL=two-factor-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"two-factor-policy.d.ts","sourceRoot":"","sources":["../../src/auth/two-factor-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,eAAO,MAAM,wBAAwB,kCAAmC,CAAC;AAEzE,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7E,eAAO,MAAM,0BAA0B,QAEa,CAAC;AAErD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED,wFAAwF;AACxF,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAElE;;;;;;;;;;;GAWG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAChC,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,OAAO,CAAC,CAGlB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIhE"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which sign-in routes need the second factor enforced by hand.
|
|
3
|
+
*
|
|
4
|
+
* Better Auth's `twoFactor` plugin hooks `/sign-in/email`, `/sign-in/username`
|
|
5
|
+
* and `/sign-in/phone-number` and nothing else. Every other sign-in route issues
|
|
6
|
+
* a session outright, second factor or not. That is fine for most of them and
|
|
7
|
+
* emphatically not fine for one:
|
|
8
|
+
*
|
|
9
|
+
* - **Magic link** — a link in an inbox, and strictly weaker than the password
|
|
10
|
+
* it stands in for. Allowing it while two-factor is on means a stolen mailbox
|
|
11
|
+
* is still enough, which is exactly what the feature is sold as preventing.
|
|
12
|
+
* **Refused.**
|
|
13
|
+
* - **Passkey** — already possession of a device, and phishing-resistant. At
|
|
14
|
+
* least as strong as password + TOTP. Allowed.
|
|
15
|
+
* - **Social** — delegates to a provider that enforces its own second factor.
|
|
16
|
+
* Allowed.
|
|
17
|
+
*
|
|
18
|
+
* This lives in the package rather than in each app because it is a security
|
|
19
|
+
* decision that has to be the same everywhere, and because getting it wrong is
|
|
20
|
+
* silent: two-factor keeps *looking* enabled while doing nothing.
|
|
21
|
+
*/
|
|
22
|
+
export const TWO_FACTOR_BLOCKED_PATHS = ["/sign-in/magic-link"];
|
|
23
|
+
export const TWO_FACTOR_BLOCKED_MESSAGE = "This account uses two-factor authentication. Sign in with your email and password, " +
|
|
24
|
+
"then enter the code from your authenticator app.";
|
|
25
|
+
export function isTwoFactorBlockedPath(path) {
|
|
26
|
+
return TWO_FACTOR_BLOCKED_PATHS.includes(path);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* True when this sign-in attempt must be refused because it would hand out a
|
|
30
|
+
* session without the second factor.
|
|
31
|
+
*
|
|
32
|
+
* The lookup is injected rather than imported: every app has its own Prisma
|
|
33
|
+
* client and its own user table, and a package that reached for one would be
|
|
34
|
+
* useless to the next app along.
|
|
35
|
+
*
|
|
36
|
+
* Unknown addresses return false. The refusal must not become an oracle
|
|
37
|
+
* answering "does this account exist, and does it have two-factor on?" for
|
|
38
|
+
* anyone with an email address and a spare minute.
|
|
39
|
+
*/
|
|
40
|
+
export async function shouldBlockForTwoFactor(path, email, lookup) {
|
|
41
|
+
if (!isTwoFactorBlockedPath(path) || !email)
|
|
42
|
+
return false;
|
|
43
|
+
return lookup(email.toLowerCase());
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Pull the shared secret out of an `otpauth://` URI, for the "can't scan it?"
|
|
47
|
+
* manual-entry path during enrolment.
|
|
48
|
+
*
|
|
49
|
+
* Returns null rather than throwing on a malformed URI — a missing manual-entry
|
|
50
|
+
* fallback is a worse enrolment screen, not a broken one.
|
|
51
|
+
*/
|
|
52
|
+
export function totpSecretFromUri(totpUri) {
|
|
53
|
+
const query = totpUri.split("?")[1];
|
|
54
|
+
if (!query)
|
|
55
|
+
return null;
|
|
56
|
+
return new URLSearchParams(query).get("secret");
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=two-factor-policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"two-factor-policy.js","sourceRoot":"","sources":["../../src/auth/two-factor-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,qBAAqB,CAAU,CAAC;AAIzE,MAAM,CAAC,MAAM,0BAA0B,GACrC,qFAAqF;IACrF,kDAAkD,CAAC;AAErD,MAAM,UAAU,sBAAsB,CAAC,IAAY;IACjD,OAAQ,wBAA8C,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AACxE,CAAC;AAKD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,IAAY,EACZ,KAAgC,EAChC,MAAuB;IAEvB,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IAC1D,OAAO,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;AACrC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAC/C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACpC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,OAAO,IAAI,eAAe,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;AAClD,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The frame every error and not-found screen shares.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately dependency-light — no providers, no theme hook, no data fetching,
|
|
6
|
+
* and no component library. It renders plain elements with the house token
|
|
7
|
+
* classes (`bg-card`, `border-border`, `text-muted-foreground`), so it looks
|
|
8
|
+
* like the app it is dropped into without importing anything from it. That
|
|
9
|
+
* matters more here than anywhere else: this renders precisely when something
|
|
10
|
+
* upstream has already failed.
|
|
11
|
+
*
|
|
12
|
+
* Anything app-specific — where the buttons go, what they say — arrives as
|
|
13
|
+
* props. The package has no opinion about your routes.
|
|
14
|
+
*/
|
|
15
|
+
export declare function ErrorShell({ code, title, children, actions, digest, }: {
|
|
16
|
+
/** Short marker above the heading — "404", "Error". */
|
|
17
|
+
code: string;
|
|
18
|
+
title: string;
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
actions: ReactNode;
|
|
21
|
+
/** Next's error digest. The one thing support can correlate to a server log. */
|
|
22
|
+
digest?: string;
|
|
23
|
+
}): import("react").JSX.Element;
|
|
24
|
+
/**
|
|
25
|
+
* Button-shaped classes for an ErrorShell action.
|
|
26
|
+
*
|
|
27
|
+
* Returned as a class string rather than a component, because the one thing
|
|
28
|
+
* this package must not do is bring a second Button into an app that already
|
|
29
|
+
* has one. Wrap your own:
|
|
30
|
+
*
|
|
31
|
+
* ```tsx
|
|
32
|
+
* <Button asChild variant="outline"><Link href="/">Home</Link></Button>
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* and reach for this only where there is no Button to wrap — which is most
|
|
36
|
+
* often a `global-error.tsx`, where importing one is exactly the thing that
|
|
37
|
+
* might have broken.
|
|
38
|
+
*/
|
|
39
|
+
export declare function errorActionClassName(variant?: "primary" | "secondary"): string;
|
|
40
|
+
//# sourceMappingURL=error-shell.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-shell.d.ts","sourceRoot":"","sources":["../../src/errors/error-shell.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,EACzB,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,OAAO,EACP,MAAM,GACP,EAAE;IACD,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,SAAS,CAAC;IACpB,OAAO,EAAE,SAAS,CAAC;IACnB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,+BAgBA;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,SAAS,GAAG,WAAyB,GAAG,MAAM,CAS3F"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The frame every error and not-found screen shares.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately dependency-light — no providers, no theme hook, no data fetching,
|
|
6
|
+
* and no component library. It renders plain elements with the house token
|
|
7
|
+
* classes (`bg-card`, `border-border`, `text-muted-foreground`), so it looks
|
|
8
|
+
* like the app it is dropped into without importing anything from it. That
|
|
9
|
+
* matters more here than anywhere else: this renders precisely when something
|
|
10
|
+
* upstream has already failed.
|
|
11
|
+
*
|
|
12
|
+
* Anything app-specific — where the buttons go, what they say — arrives as
|
|
13
|
+
* props. The package has no opinion about your routes.
|
|
14
|
+
*/
|
|
15
|
+
export function ErrorShell({ code, title, children, actions, digest, }) {
|
|
16
|
+
return (_jsx("main", { className: "flex min-h-[70vh] items-center justify-center px-6 py-16", children: _jsxs("div", { className: "w-full max-w-lg rounded-xl border border-border bg-card p-8 shadow-sm sm:p-10", children: [_jsx("p", { className: "font-mono text-xs uppercase tracking-[0.14em] text-muted-foreground", children: code }), _jsx("h1", { className: "mt-3 text-balance text-2xl font-semibold tracking-tight sm:text-3xl", children: title }), _jsx("div", { className: "mt-4 space-y-3 text-sm leading-relaxed text-muted-foreground", children: children }), _jsx("div", { className: "mt-8 flex flex-wrap gap-3", children: actions }), digest ? (_jsxs("p", { className: "mt-8 border-t border-border pt-4 font-mono text-xs text-muted-foreground", children: ["Reference ", _jsx("span", { className: "select-all text-foreground", children: digest })] })) : null] }) }));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Button-shaped classes for an ErrorShell action.
|
|
20
|
+
*
|
|
21
|
+
* Returned as a class string rather than a component, because the one thing
|
|
22
|
+
* this package must not do is bring a second Button into an app that already
|
|
23
|
+
* has one. Wrap your own:
|
|
24
|
+
*
|
|
25
|
+
* ```tsx
|
|
26
|
+
* <Button asChild variant="outline"><Link href="/">Home</Link></Button>
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* and reach for this only where there is no Button to wrap — which is most
|
|
30
|
+
* often a `global-error.tsx`, where importing one is exactly the thing that
|
|
31
|
+
* might have broken.
|
|
32
|
+
*/
|
|
33
|
+
export function errorActionClassName(variant = "secondary") {
|
|
34
|
+
const base = "inline-flex items-center justify-center rounded-lg px-5 py-2.5 text-sm font-medium transition " +
|
|
35
|
+
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring";
|
|
36
|
+
const tone = variant === "primary"
|
|
37
|
+
? "bg-primary text-primary-foreground hover:brightness-[1.06]"
|
|
38
|
+
: "border border-border text-foreground hover:bg-accent";
|
|
39
|
+
return `${base} ${tone}`;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=error-shell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-shell.js","sourceRoot":"","sources":["../../src/errors/error-shell.tsx"],"names":[],"mappings":";AAEA;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,UAAU,CAAC,EACzB,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,OAAO,EACP,MAAM,GASP;IACC,OAAO,CACL,eAAM,SAAS,EAAC,0DAA0D,YACxE,eAAK,SAAS,EAAC,+EAA+E,aAC5F,YAAG,SAAS,EAAC,qEAAqE,YAAE,IAAI,GAAK,EAC7F,aAAI,SAAS,EAAC,qEAAqE,YAAE,KAAK,GAAM,EAChG,cAAK,SAAS,EAAC,8DAA8D,YAAE,QAAQ,GAAO,EAC9F,cAAK,SAAS,EAAC,2BAA2B,YAAE,OAAO,GAAO,EACzD,MAAM,CAAC,CAAC,CAAC,CACR,aAAG,SAAS,EAAC,0EAA0E,2BAC3E,eAAM,SAAS,EAAC,4BAA4B,YAAE,MAAM,GAAQ,IACpE,CACL,CAAC,CAAC,CAAC,IAAI,IACJ,GACD,CACR,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,oBAAoB,CAAC,UAAmC,WAAW;IACjF,MAAM,IAAI,GACR,gGAAgG;QAChG,yGAAyG,CAAC;IAC5G,MAAM,IAAI,GACR,OAAO,KAAK,SAAS;QACnB,CAAC,CAAC,4DAA4D;QAC9D,CAAC,CAAC,sDAAsD,CAAC;IAC7D,OAAO,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC;AAC3B,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The last boundary, as a component.
|
|
3
|
+
*
|
|
4
|
+
* A Next `global-error.tsx` replaces the root layout, so it renders its own
|
|
5
|
+
* `<html>` and `<body>` and cannot rely on providers, fonts, or anything else
|
|
6
|
+
* the layout sets up — by the time it runs, that is what failed. Which is why
|
|
7
|
+
* this uses no utility classes and no shared components: only an inline
|
|
8
|
+
* stylesheet, so the page holds up even if the app's CSS never arrives.
|
|
9
|
+
*
|
|
10
|
+
* That constraint is also what made this the single most-duplicated file in the
|
|
11
|
+
* portfolio: eight apps carrying sixty lines of near-identical inline CSS whose
|
|
12
|
+
* only real difference was five hex values. Those five are the props.
|
|
13
|
+
*
|
|
14
|
+
* The palette follows the OS theme through `prefers-color-scheme`, since there
|
|
15
|
+
* is no theme provider left to ask.
|
|
16
|
+
*/
|
|
17
|
+
export interface GlobalErrorBrand {
|
|
18
|
+
/** Page background in light mode. */
|
|
19
|
+
background: string;
|
|
20
|
+
/** Body text in light mode. */
|
|
21
|
+
ink: string;
|
|
22
|
+
/** Secondary text in light mode. */
|
|
23
|
+
muted: string;
|
|
24
|
+
/** Hairlines in light mode. */
|
|
25
|
+
line: string;
|
|
26
|
+
/** The one colour that says which app this is. Used for the primary button. */
|
|
27
|
+
accent: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function GlobalErrorShell({ appName, brand, error, reset, homeHref, children, }: {
|
|
30
|
+
/** Used in the heading: "<appName> couldn't start". */
|
|
31
|
+
appName: string;
|
|
32
|
+
brand: GlobalErrorBrand;
|
|
33
|
+
error: Error & {
|
|
34
|
+
digest?: string;
|
|
35
|
+
};
|
|
36
|
+
reset: () => void;
|
|
37
|
+
homeHref?: string;
|
|
38
|
+
/** Replaces the default explanation, for an app that needs to say something else. */
|
|
39
|
+
children?: React.ReactNode;
|
|
40
|
+
}): import("react").JSX.Element;
|
|
41
|
+
//# sourceMappingURL=global-error-shell.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"global-error-shell.d.ts","sourceRoot":"","sources":["../../src/errors/global-error-shell.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,+BAA+B;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,KAAK,EACL,KAAK,EACL,KAAK,EACL,QAAc,EACd,QAAQ,GACT,EAAE;IACD,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,gBAAgB,CAAC;IACxB,KAAK,EAAE,KAAK,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qFAAqF;IACrF,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B,+BAmEA"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
export function GlobalErrorShell({ appName, brand, error, reset, homeHref = "/", children, }) {
|
|
4
|
+
return (_jsx("html", { lang: "en-GB", children: _jsxs("body", { children: [_jsx("style", { children: `
|
|
5
|
+
:root {
|
|
6
|
+
color-scheme: light dark;
|
|
7
|
+
--ge-bg: ${brand.background};
|
|
8
|
+
--ge-ink: ${brand.ink};
|
|
9
|
+
--ge-muted: ${brand.muted};
|
|
10
|
+
--ge-line: ${brand.line};
|
|
11
|
+
--ge-brand: ${brand.accent};
|
|
12
|
+
}
|
|
13
|
+
@media (prefers-color-scheme: dark) {
|
|
14
|
+
:root {
|
|
15
|
+
--ge-bg: #0b1220;
|
|
16
|
+
--ge-ink: #e8edf5;
|
|
17
|
+
--ge-muted: #94a3b8;
|
|
18
|
+
--ge-line: #1e293b;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
body { margin: 0; background: var(--ge-bg); color: var(--ge-ink);
|
|
22
|
+
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
23
|
+
.ge-wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 3rem 1.5rem; }
|
|
24
|
+
.ge-card { max-width: 34rem; }
|
|
25
|
+
.ge-eyebrow { margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
26
|
+
font-size: .75rem; letter-spacing: .14em; text-transform: uppercase; color: var(--ge-muted); }
|
|
27
|
+
.ge-title { margin: .75rem 0 0; font-size: 1.75rem; line-height: 1.2; font-weight: 600; }
|
|
28
|
+
.ge-body { margin: 1rem 0 0; line-height: 1.6; color: var(--ge-muted); }
|
|
29
|
+
.ge-actions { margin-top: 2rem; display: flex; gap: .75rem; flex-wrap: wrap; }
|
|
30
|
+
.ge-btn { padding: .625rem 1.25rem; border-radius: .5rem; font-size: .875rem; font-weight: 500;
|
|
31
|
+
cursor: pointer; border: 1px solid transparent; text-decoration: none; display: inline-block; }
|
|
32
|
+
.ge-btn-primary { background: var(--ge-brand); color: #ffffff; }
|
|
33
|
+
.ge-btn-secondary { border-color: var(--ge-line); color: var(--ge-ink); background: transparent; }
|
|
34
|
+
.ge-ref { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid var(--ge-line);
|
|
35
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .75rem; color: var(--ge-muted); }
|
|
36
|
+
.ge-ref span { color: var(--ge-ink); user-select: all; }
|
|
37
|
+
` }), _jsx("main", { className: "ge-wrap", children: _jsxs("div", { className: "ge-card", children: [_jsx("p", { className: "ge-eyebrow", children: "Application error" }), _jsxs("h1", { className: "ge-title", children: [appName, " couldn\u2019t start"] }), children ?? (_jsx("p", { className: "ge-body", children: "This is a fault in the app itself rather than in the page you asked for. Your data is untouched. Reloading usually clears it; if it does not, it is on us to fix, and the reference below is what finds it." })), _jsxs("div", { className: "ge-actions", children: [_jsx("button", { type: "button", className: "ge-btn ge-btn-primary", onClick: () => reset(), children: "Reload" }), _jsx("a", { href: homeHref, className: "ge-btn ge-btn-secondary", children: "Go home" })] }), error.digest ? (_jsxs("p", { className: "ge-ref", children: ["Reference ", _jsx("span", { children: error.digest })] })) : null] }) })] }) }));
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=global-error-shell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"global-error-shell.js","sourceRoot":"","sources":["../../src/errors/global-error-shell.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAgCb,MAAM,UAAU,gBAAgB,CAAC,EAC/B,OAAO,EACP,KAAK,EACL,KAAK,EACL,KAAK,EACL,QAAQ,GAAG,GAAG,EACd,QAAQ,GAUT;IACC,OAAO,CACL,eAAM,IAAI,EAAC,OAAO,YAChB,2BACE,0BAAQ;;;uBAGO,KAAK,CAAC,UAAU;wBACf,KAAK,CAAC,GAAG;0BACP,KAAK,CAAC,KAAK;yBACZ,KAAK,CAAC,IAAI;0BACT,KAAK,CAAC,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;SA0B7B,GAAS,EACV,eAAM,SAAS,EAAC,SAAS,YACvB,eAAK,SAAS,EAAC,SAAS,aACtB,YAAG,SAAS,EAAC,YAAY,kCAAsB,EAC/C,cAAI,SAAS,EAAC,UAAU,aAAE,OAAO,4BAA2B,EAC3D,QAAQ,IAAI,CACX,YAAG,SAAS,EAAC,SAAS,4NAIlB,CACL,EACD,eAAK,SAAS,EAAC,YAAY,aACzB,iBAAQ,IAAI,EAAC,QAAQ,EAAC,SAAS,EAAC,uBAAuB,EAAC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,uBAErE,EACT,YAAG,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAC,yBAAyB,wBAElD,IACA,EACL,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CACd,aAAG,SAAS,EAAC,QAAQ,2BACT,yBAAO,KAAK,CAAC,MAAM,GAAQ,IACnC,CACL,CAAC,CAAC,CAAC,IAAI,IACJ,GACD,IACF,GACF,CACR,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/errors/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/errors/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAyB,MAAM,sBAAsB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@connextar/house",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The pieces every app we build needs and none of them should own a copy of.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Connextar/cx-house.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"exports": {
|
|
16
|
+
"./errors": {
|
|
17
|
+
"types": "./dist/errors/index.d.ts",
|
|
18
|
+
"default": "./dist/errors/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./auth": {
|
|
21
|
+
"types": "./dist/auth/index.d.ts",
|
|
22
|
+
"default": "./dist/auth/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./account": {
|
|
25
|
+
"types": "./dist/account/index.d.ts",
|
|
26
|
+
"default": "./dist/account/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"format:check": "prettier --check \"src/**/*.{ts,tsx}\"",
|
|
32
|
+
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
|
33
|
+
"lint:check": "eslint ./src",
|
|
34
|
+
"lint": "eslint ./src --fix",
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"check": "npm run format:check && npm run typecheck && npm run lint:check",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest",
|
|
39
|
+
"build": "rm -rf dist && tsc --project tsconfig.build.json",
|
|
40
|
+
"verify": "npm run check && npm run test && npm run build",
|
|
41
|
+
"prepublishOnly": "npm run verify"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"react": "^19.0.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@eslint/js": "^9.39.5",
|
|
48
|
+
"@types/react": "^19.2.0",
|
|
49
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
50
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
51
|
+
"eslint": "^9.0.0",
|
|
52
|
+
"prettier": "^3.0.0",
|
|
53
|
+
"typescript": "^5.7.0",
|
|
54
|
+
"typescript-eslint": "^8.69.0",
|
|
55
|
+
"vitest": "^4.0.0"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
},
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": ">=20"
|
|
62
|
+
}
|
|
63
|
+
}
|