@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 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,2 @@
1
+ export { ACCOUNT_EXPORT_NOTICE, ACCOUNT_EXPORT_SCHEMA_VERSION, deletionBlockedMessage, deletionDestroysOrganisations, } from "./data-contract";
2
+ //# sourceMappingURL=index.js.map
@@ -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,2 @@
1
+ export { TWO_FACTOR_BLOCKED_MESSAGE, TWO_FACTOR_BLOCKED_PATHS, isTwoFactorBlockedPath, shouldBlockForTwoFactor, totpSecretFromUri, type TwoFactorBlockedPath, type TwoFactorLookup, } from "./two-factor-policy";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -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,2 @@
1
+ export { TWO_FACTOR_BLOCKED_MESSAGE, TWO_FACTOR_BLOCKED_PATHS, isTwoFactorBlockedPath, shouldBlockForTwoFactor, totpSecretFromUri, } from "./two-factor-policy";
2
+ //# sourceMappingURL=index.js.map
@@ -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,3 @@
1
+ export { ErrorShell, errorActionClassName } from "./error-shell";
2
+ export { GlobalErrorShell, type GlobalErrorBrand } from "./global-error-shell";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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,3 @@
1
+ export { ErrorShell, errorActionClassName } from "./error-shell";
2
+ export { GlobalErrorShell } from "./global-error-shell";
3
+ //# sourceMappingURL=index.js.map
@@ -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
+ }