@guard-rail/core 0.3.0 → 0.5.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 +65 -28
- package/dist/adapters/supabase/auth.js +50 -0
- package/dist/adapters/supabase/auth.js.map +1 -1
- package/dist/cli/init-db.d.ts +1 -0
- package/dist/cli/init-db.js +121 -0
- package/dist/cli/init-db.js.map +1 -0
- package/dist/core/adapter-types.d.ts +43 -1
- package/dist/core/defs.d.ts +43 -2
- package/dist/core/defs.js +29 -2
- package/dist/core/defs.js.map +1 -1
- package/dist/core/types.d.ts +8 -1
- package/dist/index.client.d.ts +3 -3
- package/dist/index.client.js +5 -1
- package/dist/index.client.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/react/Admin.js +83 -7
- package/dist/react/Admin.js.map +1 -1
- package/dist/react/AuthRedirectSync.d.ts +20 -0
- package/dist/react/AuthRedirectSync.js +38 -0
- package/dist/react/AuthRedirectSync.js.map +1 -0
- package/dist/react/GRail/client.js +5 -1
- package/dist/react/GRail/client.js.map +1 -1
- package/dist/react/Provider.js +5 -1
- package/dist/react/Provider.js.map +1 -1
- package/dist/react/Syncer.d.ts +10 -8
- package/dist/react/Syncer.js +8 -6
- package/dist/react/Syncer.js.map +1 -1
- package/dist/react/useAnchorRect.d.ts +17 -1
- package/dist/react/useAnchorRect.js +37 -9
- package/dist/react/useAnchorRect.js.map +1 -1
- package/dist/server/actions.d.ts +31 -3
- package/dist/server/actions.js +45 -1
- package/dist/server/actions.js.map +1 -1
- package/dist/server/wrapper.js +7 -2
- package/dist/server/wrapper.js.map +1 -1
- package/package.json +3 -2
- package/scripts/postinstall.mjs +4 -4
package/README.md
CHANGED
|
@@ -25,14 +25,20 @@ pnpm add @guard-rail/core @supabase/ssr @supabase/supabase-js
|
|
|
25
25
|
|
|
26
26
|
Set up the database:
|
|
27
27
|
|
|
28
|
-
1.
|
|
29
|
-
project's `supabase/migrations
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
1. Run `npx guardrail-init-db --push`. It copies the package's migration(s)
|
|
29
|
+
into your own project's `supabase/migrations/` (skipping any already
|
|
30
|
+
there — safe to re-run after upgrading), runs `supabase init` if needed,
|
|
31
|
+
adds `guardrail` to `config.toml`'s exposed schemas for local dev, and
|
|
32
|
+
applies the migration with `supabase db push`. Leave off `--push` to copy
|
|
33
|
+
without applying. No Supabase CLI at all? Paste the migration file's
|
|
34
|
+
contents into the SQL editor instead — same statements, just without the
|
|
35
|
+
CLI's version tracking.
|
|
36
|
+
2. **Required for a hosted project:** also add `guardrail` under
|
|
37
|
+
**Settings > API > Exposed schemas** in the Supabase dashboard —
|
|
38
|
+
`config.toml`'s schemas list only governs local dev. PostgREST only
|
|
39
|
+
serves schemas on that list; every table below is otherwise invisible to
|
|
40
|
+
the client with a "not found in schema cache" error that looks identical
|
|
41
|
+
to a missing table.
|
|
36
42
|
|
|
37
43
|
Guardrail's tables live in their own `guardrail` Postgres schema, not `public`
|
|
38
44
|
— so a `supabase db reset`, a broad `DROP ... CASCADE`, or a migration tool
|
|
@@ -114,7 +120,15 @@ Links take `as="a"` (so `href` type-checks) and edit both the text and the URL:
|
|
|
114
120
|
```
|
|
115
121
|
|
|
116
122
|
To edit, visit any page with `?edit=true`, sign in with a Supabase user, and
|
|
117
|
-
click a field.
|
|
123
|
+
click a field. `?edit=true` itself only grants a short (~10 minute) window to
|
|
124
|
+
reach the sign-in form — a normal visitor never sees any Guardrail chrome, and
|
|
125
|
+
a stray copy of that window (a shared computer, an old tab) self-expires
|
|
126
|
+
rather than showing a sign-in bar indefinitely. It's a dismissable prompt, not
|
|
127
|
+
a standing invitation: a signed-in editor's toolbar isn't on that timer at
|
|
128
|
+
all, and persists for as long as their real session does, with no need to
|
|
129
|
+
revisit `?edit=true` again after the first sign-in. `?edit=false` explicitly
|
|
130
|
+
hides the toolbar (also for ~10 minutes) if you want to preview the site
|
|
131
|
+
without it while still signed in.
|
|
118
132
|
|
|
119
133
|
### In Client Components
|
|
120
134
|
|
|
@@ -192,26 +206,42 @@ export default withGuardrail(RootLayout, {
|
|
|
192
206
|
});
|
|
193
207
|
```
|
|
194
208
|
|
|
195
|
-
**Invite emails (Supabase).** Inviting someone
|
|
196
|
-
|
|
197
|
-
|
|
209
|
+
**Invite emails (Supabase).** Inviting someone emails them a sign-in link
|
|
210
|
+
that creates their account on first use — this works out of the box, no
|
|
211
|
+
route or email template changes needed. A small always-mounted component
|
|
212
|
+
notices the link's tokens (in the URL hash, a `code`, or a `token_hash` —
|
|
213
|
+
whichever style your project's templates happen to produce, including the
|
|
214
|
+
unedited default) and completes the session automatically.
|
|
215
|
+
|
|
216
|
+
Someone arriving this way has a valid session but no password yet, so they
|
|
217
|
+
get a **Set Password** prompt in place of the usual toolbar — skippable,
|
|
218
|
+
since a magic link works fine on its own indefinitely if they'd rather not.
|
|
219
|
+
The same prompt appears after a password-recovery link.
|
|
220
|
+
|
|
221
|
+
Two things still worth knowing:
|
|
222
|
+
|
|
223
|
+
- **Auth → URL Configuration**, in the Supabase dashboard: Site URL and
|
|
224
|
+
Redirect URLs must actually match wherever your app runs, including in
|
|
225
|
+
local dev — a mismatch here makes Supabase silently redirect to whatever
|
|
226
|
+
Site URL *is* configured instead of erroring, which reads exactly like the
|
|
227
|
+
link "not working" for no visible reason.
|
|
228
|
+
- **Auth → SMTP**: Supabase's built-in mailer allows only a few emails an
|
|
229
|
+
hour. Configure custom SMTP (Resend, Postmark, SES) before relying on
|
|
230
|
+
invites. Submitting the same address again re-sends a pending invite.
|
|
231
|
+
|
|
232
|
+
Prefer a route that verifies server-side rather than relying on client JS to
|
|
233
|
+
notice the redirect? `supabaseConfirmRoute` still works exactly as before —
|
|
234
|
+
mount it and point a custom email template's link at
|
|
235
|
+
`{{ .SiteURL }}/guardrail/confirm?token_hash={{ .TokenHash }}&type=email`:
|
|
198
236
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
```html
|
|
208
|
-
<a href="{{ .SiteURL }}/guardrail/confirm?token_hash={{ .TokenHash }}&type=email">Sign in</a>
|
|
209
|
-
```
|
|
210
|
-
The default `{{ .ConfirmationURL }}` uses a PKCE code that only works in
|
|
211
|
-
the browser that requested it — the admin's, not the invitee's.
|
|
212
|
-
4. **Auth → SMTP** — Supabase's built-in mailer allows only a few emails an
|
|
213
|
-
hour. Configure custom SMTP (Resend, Postmark, SES) before relying on
|
|
214
|
-
invites. Submitting the same address again re-sends a pending invite.
|
|
237
|
+
```ts
|
|
238
|
+
// app/guardrail/confirm/route.ts
|
|
239
|
+
export const GET = supabaseConfirmRoute({ url, anonKey });
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The default `{{ .ConfirmationURL }}` uses a PKCE code that only works in the
|
|
243
|
+
browser that requested it — the admin's, not the invitee's — so a custom
|
|
244
|
+
template needs the token hash form above either way.
|
|
215
245
|
|
|
216
246
|
**Bootstrapping.** A site with no members has nobody who can add members, and
|
|
217
247
|
there is deliberately no in-app way to grant yourself a role — that would mean
|
|
@@ -434,6 +464,13 @@ const clerkAuth: GRAuthAdapter = {
|
|
|
434
464
|
};
|
|
435
465
|
```
|
|
436
466
|
|
|
467
|
+
`completeAuthRedirect` and `setPassword` are optional too, and go together —
|
|
468
|
+
an adapter with no concept of a redirect-carried session (an OAuth provider
|
|
469
|
+
whose own SDK already handles its callback route, say) omits both, and links
|
|
470
|
+
of that kind pass through untouched. Implement `completeAuthRedirect` to
|
|
471
|
+
support them, returning `needsPassword: true` for a session that has none
|
|
472
|
+
yet; `setPassword` is what the resulting prompt calls.
|
|
473
|
+
|
|
437
474
|
`getSession()` is the sole authorization check for writes. It must validate the
|
|
438
475
|
session against the provider — never trust the presence of a cookie.
|
|
439
476
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { anonClient, assertCredentials, sessionClient } from "./client";
|
|
2
|
+
const NEEDS_PASSWORD_TYPES = /* @__PURE__ */ new Set(["invite", "recovery", "magiclink"]);
|
|
2
3
|
function supabaseAuth(creds) {
|
|
3
4
|
assertCredentials(creds);
|
|
4
5
|
return {
|
|
@@ -48,6 +49,55 @@ function supabaseAuth(creds) {
|
|
|
48
49
|
async logout() {
|
|
49
50
|
const client = await sessionClient(creds);
|
|
50
51
|
await client.auth.signOut();
|
|
52
|
+
},
|
|
53
|
+
/**
|
|
54
|
+
* Whichever of hash/code/tokenHash is present decides the exchange —
|
|
55
|
+
* these correspond to Supabase's three redirect styles (legacy implicit
|
|
56
|
+
* grant, PKCE, and the token_hash style Guardrail's own confirm route
|
|
57
|
+
* already used), so this handles a link regardless of which one a
|
|
58
|
+
* project's email templates happen to produce, including the
|
|
59
|
+
* unconfigured default (implicit grant, hash-based — what an unedited
|
|
60
|
+
* Supabase project actually sends).
|
|
61
|
+
*
|
|
62
|
+
* Goes through sessionClient(), the same cookie-writing client
|
|
63
|
+
* everything else in this adapter uses — setSession/exchangeCodeForSession/
|
|
64
|
+
* verifyOtp all persist the resulting session through its existing
|
|
65
|
+
* getAll/setAll bridge, so this needs no browser-side Supabase client of
|
|
66
|
+
* its own.
|
|
67
|
+
*/
|
|
68
|
+
async completeAuthRedirect({ hash, code, tokenHash, type }) {
|
|
69
|
+
const client = await sessionClient(creds);
|
|
70
|
+
if (hash) {
|
|
71
|
+
const hashParams = new URLSearchParams(hash);
|
|
72
|
+
const access_token = hashParams.get("access_token");
|
|
73
|
+
const refresh_token = hashParams.get("refresh_token");
|
|
74
|
+
const hashType = hashParams.get("type") ?? type;
|
|
75
|
+
if (!access_token || !refresh_token) {
|
|
76
|
+
return { success: false, error: "Redirect link is missing its tokens." };
|
|
77
|
+
}
|
|
78
|
+
const { error } = await client.auth.setSession({ access_token, refresh_token });
|
|
79
|
+
if (error) return { success: false, error: error.message };
|
|
80
|
+
return { success: true, needsPassword: NEEDS_PASSWORD_TYPES.has(hashType ?? "") };
|
|
81
|
+
}
|
|
82
|
+
if (code) {
|
|
83
|
+
const { error } = await client.auth.exchangeCodeForSession(code);
|
|
84
|
+
if (error) return { success: false, error: error.message };
|
|
85
|
+
return { success: true, needsPassword: NEEDS_PASSWORD_TYPES.has(type ?? "") };
|
|
86
|
+
}
|
|
87
|
+
if (tokenHash && type) {
|
|
88
|
+
const { error } = await client.auth.verifyOtp({
|
|
89
|
+
type,
|
|
90
|
+
token_hash: tokenHash
|
|
91
|
+
});
|
|
92
|
+
if (error) return { success: false, error: error.message };
|
|
93
|
+
return { success: true, needsPassword: NEEDS_PASSWORD_TYPES.has(type) };
|
|
94
|
+
}
|
|
95
|
+
return { success: false, error: "No redirect data to complete." };
|
|
96
|
+
},
|
|
97
|
+
async setPassword(password) {
|
|
98
|
+
const client = await sessionClient(creds);
|
|
99
|
+
const { error } = await client.auth.updateUser({ password });
|
|
100
|
+
return error ? { success: false, error: error.message } : { success: true };
|
|
51
101
|
}
|
|
52
102
|
};
|
|
53
103
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/adapters/supabase/auth.ts"],"sourcesContent":["import { anonClient, assertCredentials, sessionClient } from \"./client\";\nimport type { SupabaseCredentials } from \"./client\";\nimport type {
|
|
1
|
+
{"version":3,"sources":["../../../src/adapters/supabase/auth.ts"],"sourcesContent":["import type { EmailOtpType } from \"@supabase/supabase-js\";\nimport { anonClient, assertCredentials, sessionClient } from \"./client\";\nimport type { SupabaseCredentials } from \"./client\";\nimport type {\n AuthRedirectParams,\n GRAuthAdapter,\n GRSession,\n} from \"../../core/adapter-types\";\n\n// \"invite\" covers Supabase's own admin-API invite flow; \"recovery\" is\n// forgot-password. \"magiclink\" belongs here too — deliberately, not an\n// oversight — because inviteUser() below sends one (see its own comment for\n// why) rather than an \"invite\"-typed email, so this is the type Guardrail's\n// actual, real invite flow produces. A returning magic-link user who already\n// has a password sees this redundantly, but it's skippable, and the\n// alternative — never prompting the person this feature exists for — isn't\n// an acceptable trade to avoid that.\nconst NEEDS_PASSWORD_TYPES = new Set([\"invite\", \"recovery\", \"magiclink\"]);\n\nexport function supabaseAuth(creds: SupabaseCredentials): GRAuthAdapter {\n assertCredentials(creds);\n\n return {\n async authenticate(email, password) {\n const client = await sessionClient(creds);\n const { data, error } = await client.auth.signInWithPassword({\n email,\n password,\n });\n\n if (error) return { success: false, error: error.message };\n if (!data.user) return { success: false, error: \"User not found.\" };\n\n return {\n success: true,\n session: { userId: data.user.id, email: data.user.email },\n };\n },\n\n /**\n * Uses getUser(), which revalidates the JWT against the Supabase auth\n * server. getSession() only decodes the cookie, so it would trust a forged\n * token.\n */\n async getSession(): Promise<GRSession | null> {\n const client = await sessionClient(creds);\n const { data, error } = await client.auth.getUser();\n if (error || !data.user) return null;\n return { userId: data.user.id, email: data.user.email };\n },\n\n /**\n * Sends a sign-in link that creates the account on first use.\n *\n * Requested through the cookie-free client on purpose. A session client\n * would use PKCE and drop the code verifier into the *admin's* browser,\n * so the link would fail when the invitee opened it anywhere else. The\n * email template must point at the confirm route with {{ .TokenHash }},\n * which verifies server-side and needs no verifier.\n */\n async inviteUser(email, options) {\n const { error } = await anonClient(creds).auth.signInWithOtp({\n email,\n options: {\n shouldCreateUser: true,\n ...(options?.redirectTo ? { emailRedirectTo: options.redirectTo } : {}),\n },\n });\n return error\n ? { success: false, error: error.message }\n : { success: true };\n },\n\n async logout() {\n const client = await sessionClient(creds);\n await client.auth.signOut();\n },\n\n /**\n * Whichever of hash/code/tokenHash is present decides the exchange —\n * these correspond to Supabase's three redirect styles (legacy implicit\n * grant, PKCE, and the token_hash style Guardrail's own confirm route\n * already used), so this handles a link regardless of which one a\n * project's email templates happen to produce, including the\n * unconfigured default (implicit grant, hash-based — what an unedited\n * Supabase project actually sends).\n *\n * Goes through sessionClient(), the same cookie-writing client\n * everything else in this adapter uses — setSession/exchangeCodeForSession/\n * verifyOtp all persist the resulting session through its existing\n * getAll/setAll bridge, so this needs no browser-side Supabase client of\n * its own.\n */\n async completeAuthRedirect({ hash, code, tokenHash, type }: AuthRedirectParams) {\n const client = await sessionClient(creds);\n\n if (hash) {\n const hashParams = new URLSearchParams(hash);\n const access_token = hashParams.get(\"access_token\");\n const refresh_token = hashParams.get(\"refresh_token\");\n const hashType = hashParams.get(\"type\") ?? type;\n if (!access_token || !refresh_token) {\n return { success: false, error: \"Redirect link is missing its tokens.\" };\n }\n const { error } = await client.auth.setSession({ access_token, refresh_token });\n if (error) return { success: false, error: error.message };\n return { success: true, needsPassword: NEEDS_PASSWORD_TYPES.has(hashType ?? \"\") };\n }\n\n if (code) {\n const { error } = await client.auth.exchangeCodeForSession(code);\n if (error) return { success: false, error: error.message };\n return { success: true, needsPassword: NEEDS_PASSWORD_TYPES.has(type ?? \"\") };\n }\n\n if (tokenHash && type) {\n const { error } = await client.auth.verifyOtp({\n type: type as EmailOtpType,\n token_hash: tokenHash,\n });\n if (error) return { success: false, error: error.message };\n return { success: true, needsPassword: NEEDS_PASSWORD_TYPES.has(type) };\n }\n\n return { success: false, error: \"No redirect data to complete.\" };\n },\n\n async setPassword(password: string) {\n const client = await sessionClient(creds);\n const { error } = await client.auth.updateUser({ password });\n return error ? { success: false, error: error.message } : { success: true };\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
copyFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
writeFileSync
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
function commandExists(cmd) {
|
|
14
|
+
const res = process.platform === "win32" ? spawnSync("where", [cmd]) : spawnSync("which", [cmd]);
|
|
15
|
+
return res.status === 0;
|
|
16
|
+
}
|
|
17
|
+
function run(cmd, args) {
|
|
18
|
+
const res = spawnSync(cmd, args, { stdio: "inherit" });
|
|
19
|
+
if (res.status !== 0) {
|
|
20
|
+
throw new Error(`${cmd} ${args.join(" ")} exited with code ${res.status}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function fail(message) {
|
|
24
|
+
console.error(`
|
|
25
|
+
${message}
|
|
26
|
+
`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const USAGE = `Usage:
|
|
30
|
+
guardrail-init-db [--push]
|
|
31
|
+
|
|
32
|
+
Copies Guardrail's migration(s) from this package into ./supabase/migrations/
|
|
33
|
+
(skipping any already present there), runs \`supabase init\` if this project
|
|
34
|
+
has no supabase/config.toml yet, and adds "guardrail" to its exposed schemas
|
|
35
|
+
for local dev.
|
|
36
|
+
|
|
37
|
+
--push Also run \`supabase db push\` once the files are in place. Without it,
|
|
38
|
+
the migration is copied but not applied \u2014 run \`supabase db push\`
|
|
39
|
+
yourself (after \`supabase link\`, if this project isn't linked yet).`;
|
|
40
|
+
function patchExposedSchemas(configPath) {
|
|
41
|
+
const content = readFileSync(configPath, "utf8");
|
|
42
|
+
const patched = content.replace(
|
|
43
|
+
/(\[api\][\s\S]*?schemas\s*=\s*\[)([^\]]*)(\])/,
|
|
44
|
+
(match, pre, inner, post) => {
|
|
45
|
+
if (inner.includes('"guardrail"')) return match;
|
|
46
|
+
const trimmed = inner.trimEnd();
|
|
47
|
+
const sep = trimmed === "" || trimmed.endsWith(",") ? " " : ", ";
|
|
48
|
+
return `${pre}${inner}${sep}"guardrail"${post}`;
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
if (patched === content && !content.includes('"guardrail"')) {
|
|
52
|
+
console.warn(
|
|
53
|
+
'Could not find the [api] schemas setting in supabase/config.toml to patch automatically \u2014 add "guardrail" to it yourself before `supabase start`.'
|
|
54
|
+
);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
writeFileSync(configPath, patched);
|
|
58
|
+
}
|
|
59
|
+
async function main() {
|
|
60
|
+
const args = process.argv.slice(2);
|
|
61
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
62
|
+
console.log(USAGE);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const push = args.includes("--push");
|
|
66
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
67
|
+
const pkgRoot = path.join(__dirname, "..", "..");
|
|
68
|
+
const srcDir = path.join(pkgRoot, "supabase", "migrations");
|
|
69
|
+
if (!existsSync(srcDir)) {
|
|
70
|
+
fail(`No bundled migrations found at ${srcDir} \u2014 is @guard-rail/core installed correctly?`);
|
|
71
|
+
}
|
|
72
|
+
const destDir = path.join(process.cwd(), "supabase", "migrations");
|
|
73
|
+
mkdirSync(destDir, { recursive: true });
|
|
74
|
+
const existing = new Set(readdirSync(destDir));
|
|
75
|
+
const copied = [];
|
|
76
|
+
for (const file of readdirSync(srcDir)) {
|
|
77
|
+
if (existing.has(file)) continue;
|
|
78
|
+
copyFileSync(path.join(srcDir, file), path.join(destDir, file));
|
|
79
|
+
copied.push(file);
|
|
80
|
+
}
|
|
81
|
+
if (copied.length === 0) {
|
|
82
|
+
console.log("supabase/migrations/ is already up to date \u2014 nothing to copy.");
|
|
83
|
+
} else {
|
|
84
|
+
console.log(`Copied ${copied.length} migration${copied.length === 1 ? "" : "s"}:`);
|
|
85
|
+
for (const file of copied) console.log(` supabase/migrations/${file}`);
|
|
86
|
+
}
|
|
87
|
+
if (!commandExists("supabase")) {
|
|
88
|
+
console.log(
|
|
89
|
+
"\nSupabase CLI not found \u2014 apply the migration yourself (paste it into the SQL editor, or install the CLI and run `supabase db push`)."
|
|
90
|
+
);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const configPath = path.join(process.cwd(), "supabase", "config.toml");
|
|
94
|
+
if (!existsSync(configPath)) {
|
|
95
|
+
try {
|
|
96
|
+
run("supabase", ["init"]);
|
|
97
|
+
} catch {
|
|
98
|
+
console.warn("\n`supabase init` failed \u2014 you can run it yourself.");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (existsSync(configPath)) patchExposedSchemas(configPath);
|
|
103
|
+
if (!push) {
|
|
104
|
+
console.log(
|
|
105
|
+
"\nRun `guardrail-init-db --push` (or `supabase db push` yourself) to apply it."
|
|
106
|
+
);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
run("supabase", ["db", "push"]);
|
|
111
|
+
} catch {
|
|
112
|
+
console.warn(
|
|
113
|
+
"\n`supabase db push` failed \u2014 if this project isn't linked yet, run `supabase link` first, then `supabase db push`."
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
main().catch((err) => {
|
|
118
|
+
console.error(err);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
});
|
|
121
|
+
//# sourceMappingURL=init-db.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/cli/init-db.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Copies Guardrail's SQL migration(s) into the current project's own\n * supabase/migrations/, then (optionally) applies them — the step\n * create-guardrail already automates for a brand-new scaffold (see\n * copyMigration()/maybeLinkAndPush() in that package), but has no\n * equivalent for an existing Next.js + Supabase app that just ran\n * `npm install @guard-rail/core`. Until now, postinstall.mjs pointed at\n * doing this by hand: \"Copy\n * node_modules/@guard-rail/core/supabase/migrations/*.sql into your own\n * supabase/migrations/, then `supabase db push`.\"\n *\n * Copies by filename, skipping anything already present in the destination\n * — safe to re-run after upgrading the package: it only ever adds\n * migrations this version ships that a prior run didn't already copy, and\n * never overwrites one that's already there.\n *\n * Deliberately non-interactive (flags only, no prompts), unlike\n * create-guardrail's wizard-style prompts — this is meant to be a single\n * command a developer (or a script) runs once against a project that\n * already exists, not a guided setup flow.\n */\nimport { spawnSync } from \"node:child_process\";\nimport {\n existsSync,\n mkdirSync,\n copyFileSync,\n readdirSync,\n readFileSync,\n writeFileSync,\n} from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nfunction commandExists(cmd: string): boolean {\n // See create-guardrail's util.ts for why `which`/`where` over `command -v`\n // with shell:true — no shell needed, no Node deprecation warning.\n const res =\n process.platform === \"win32\" ? spawnSync(\"where\", [cmd]) : spawnSync(\"which\", [cmd]);\n return res.status === 0;\n}\n\n/** Throws with the failing command's name — every call site decides whether that's fatal. */\nfunction run(cmd: string, args: string[]): void {\n const res = spawnSync(cmd, args, { stdio: \"inherit\" });\n if (res.status !== 0) {\n throw new Error(`${cmd} ${args.join(\" \")} exited with code ${res.status}`);\n }\n}\n\nfunction fail(message: string): never {\n console.error(`\\n${message}\\n`);\n process.exit(1);\n}\n\nconst USAGE = `Usage:\n guardrail-init-db [--push]\n\nCopies Guardrail's migration(s) from this package into ./supabase/migrations/\n(skipping any already present there), runs \\`supabase init\\` if this project\nhas no supabase/config.toml yet, and adds \"guardrail\" to its exposed schemas\nfor local dev.\n\n--push Also run \\`supabase db push\\` once the files are in place. Without it,\n the migration is copied but not applied — run \\`supabase db push\\`\n yourself (after \\`supabase link\\`, if this project isn't linked yet).`;\n\n/**\n * Idempotent by design (a no-op if \"guardrail\" is already listed) — safe to\n * run on every invocation rather than only the first, including against a\n * config.toml a developer has since hand-edited.\n */\nfunction patchExposedSchemas(configPath: string): void {\n const content = readFileSync(configPath, \"utf8\");\n const patched = content.replace(\n /(\\[api\\][\\s\\S]*?schemas\\s*=\\s*\\[)([^\\]]*)(\\])/,\n (match, pre: string, inner: string, post: string) => {\n if (inner.includes('\"guardrail\"')) return match;\n const trimmed = inner.trimEnd();\n const sep = trimmed === \"\" || trimmed.endsWith(\",\") ? \" \" : \", \";\n return `${pre}${inner}${sep}\"guardrail\"${post}`;\n },\n );\n\n if (patched === content && !content.includes('\"guardrail\"')) {\n console.warn(\n 'Could not find the [api] schemas setting in supabase/config.toml to patch ' +\n 'automatically — add \"guardrail\" to it yourself before `supabase start`.',\n );\n return;\n }\n writeFileSync(configPath, patched);\n}\n\nasync function main() {\n const args = process.argv.slice(2);\n if (args.includes(\"--help\") || args.includes(\"-h\")) {\n console.log(USAGE);\n return;\n }\n const push = args.includes(\"--push\");\n\n // This file's own location, not the invoking project's node_modules —\n // resolves the migrations this installed package version ships,\n // regardless of how the command was invoked (npx, a package.json script,\n // a workspace link).\n const __dirname = path.dirname(fileURLToPath(import.meta.url));\n const pkgRoot = path.join(__dirname, \"..\", \"..\");\n const srcDir = path.join(pkgRoot, \"supabase\", \"migrations\");\n if (!existsSync(srcDir)) {\n fail(`No bundled migrations found at ${srcDir} — is @guard-rail/core installed correctly?`);\n }\n\n const destDir = path.join(process.cwd(), \"supabase\", \"migrations\");\n mkdirSync(destDir, { recursive: true });\n const existing = new Set(readdirSync(destDir));\n\n const copied: string[] = [];\n for (const file of readdirSync(srcDir)) {\n if (existing.has(file)) continue;\n copyFileSync(path.join(srcDir, file), path.join(destDir, file));\n copied.push(file);\n }\n\n if (copied.length === 0) {\n console.log(\"supabase/migrations/ is already up to date — nothing to copy.\");\n } else {\n console.log(`Copied ${copied.length} migration${copied.length === 1 ? \"\" : \"s\"}:`);\n for (const file of copied) console.log(` supabase/migrations/${file}`);\n }\n\n if (!commandExists(\"supabase\")) {\n console.log(\n \"\\nSupabase CLI not found — apply the migration yourself (paste it into the SQL \" +\n \"editor, or install the CLI and run `supabase db push`).\",\n );\n return;\n }\n\n const configPath = path.join(process.cwd(), \"supabase\", \"config.toml\");\n if (!existsSync(configPath)) {\n try {\n run(\"supabase\", [\"init\"]);\n } catch {\n console.warn(\"\\n`supabase init` failed — you can run it yourself.\");\n return;\n }\n }\n if (existsSync(configPath)) patchExposedSchemas(configPath);\n\n if (!push) {\n console.log(\n \"\\nRun `guardrail-init-db --push` (or `supabase db push` yourself) to apply it.\",\n );\n return;\n }\n\n try {\n run(\"supabase\", [\"db\", \"push\"]);\n } catch {\n console.warn(\n \"\\n`supabase db push` failed — if this project isn't linked yet, run `supabase \" +\n \"link` first, then `supabase db push`.\",\n );\n }\n}\n\nmain().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n"],"mappings":"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","names":[]}
|
|
@@ -97,6 +97,48 @@ interface GRAuthAdapter {
|
|
|
97
97
|
*/
|
|
98
98
|
getSession: () => Promise<GRSession | null>;
|
|
99
99
|
logout: () => Promise<void>;
|
|
100
|
+
/**
|
|
101
|
+
* Completes a session that arrived via a redirect link — an invite, magic
|
|
102
|
+
* link, or password-recovery email — rather than the normal sign-in form.
|
|
103
|
+
* Optional: an adapter with no such concept (an OAuth provider whose own
|
|
104
|
+
* SDK already handles its callback route, say) simply omits it, and links
|
|
105
|
+
* of this kind pass through Guardrail untouched.
|
|
106
|
+
*
|
|
107
|
+
* Whatever the client found in the URL is handed through as-is — which of
|
|
108
|
+
* `hash`/`code`/`tokenHash` is present depends on the provider's own
|
|
109
|
+
* redirect style, not a choice Guardrail's core makes. `needsPassword` in
|
|
110
|
+
* the result tells the caller to prompt for one: an invited or
|
|
111
|
+
* password-recovering person has a valid session but no password yet.
|
|
112
|
+
*/
|
|
113
|
+
completeAuthRedirect?: (params: AuthRedirectParams) => Promise<{
|
|
114
|
+
success: boolean;
|
|
115
|
+
needsPassword?: boolean;
|
|
116
|
+
error?: string;
|
|
117
|
+
}>;
|
|
118
|
+
/**
|
|
119
|
+
* Sets the current session's password — the other half of
|
|
120
|
+
* `completeAuthRedirect`'s `needsPassword`. Optional for the same reason
|
|
121
|
+
* `authenticate` is: only meaningful for a password-based adapter.
|
|
122
|
+
*/
|
|
123
|
+
setPassword?: (password: string) => Promise<{
|
|
124
|
+
success: boolean;
|
|
125
|
+
error?: string;
|
|
126
|
+
}>;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Raw redirect data as the browser found it — see completeAuthRedirect.
|
|
130
|
+
* Hash-based tokens never reach the server on their own; the browser has to
|
|
131
|
+
* read them and hand them here.
|
|
132
|
+
*/
|
|
133
|
+
interface AuthRedirectParams {
|
|
134
|
+
/** window.location.hash, minus its leading '#' (the implicit-grant style). */
|
|
135
|
+
hash?: string;
|
|
136
|
+
/** The `code` query param (the PKCE style). */
|
|
137
|
+
code?: string;
|
|
138
|
+
/** The `token_hash` query param (Guardrail's own documented confirm-route style, detected here too so that route stays opt-in rather than required). */
|
|
139
|
+
tokenHash?: string;
|
|
140
|
+
/** Accompanies `code` or `tokenHash` — e.g. "invite", "recovery", "magiclink". */
|
|
141
|
+
type?: string;
|
|
100
142
|
}
|
|
101
143
|
type AuthResult = {
|
|
102
144
|
success: true;
|
|
@@ -243,4 +285,4 @@ interface GRConfig {
|
|
|
243
285
|
prefetch?: false | GRPrefetch;
|
|
244
286
|
}
|
|
245
287
|
|
|
246
|
-
export type { AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget };
|
|
288
|
+
export type { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget };
|
package/dist/core/defs.d.ts
CHANGED
|
@@ -1,7 +1,48 @@
|
|
|
1
1
|
declare const GR_COOKIES: {
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Signals intent to edit. Carries no authority on its own — every write
|
|
4
|
+
* still goes through a real, server-verified role check, independent of
|
|
5
|
+
* this — but it does decide whether the admin chrome (the sign-in bar, or
|
|
6
|
+
* the toolbar) renders at all, so it is not harmless either: a stale copy
|
|
7
|
+
* of it is a real, if low-stakes, exposure ("there's a CMS here") to
|
|
8
|
+
* whoever's browser it's sitting in next, not just its original visitor.
|
|
9
|
+
*
|
|
10
|
+
* Values: "true" (show chrome — the short-lived grace window below, for
|
|
11
|
+
* someone not yet authenticated to reach the sign-in form), "false"
|
|
12
|
+
* (explicitly suppressed, e.g. ?edit=false previewing without chrome while
|
|
13
|
+
* still signed in), or absent. Absent defers entirely to whether a real
|
|
14
|
+
* session exists (see withGuardrail) — an authenticated editor's chrome
|
|
15
|
+
* persists for as long as their actual session does, with no separate
|
|
16
|
+
* timer of its own to fall out of sync with it.
|
|
17
|
+
*/
|
|
3
18
|
readonly EDIT_MODE: "gr_edit_mode";
|
|
19
|
+
/**
|
|
20
|
+
* Set server-side the moment a redirect-carried session (an invite or
|
|
21
|
+
* password-recovery link) completes, so the next render — which has no
|
|
22
|
+
* other way to know that just happened — prompts to set a password
|
|
23
|
+
* instead of showing the normal toolbar. Cleared once they do, or once
|
|
24
|
+
* they explicitly skip.
|
|
25
|
+
*/
|
|
26
|
+
readonly NEEDS_PASSWORD: "gr_needs_password";
|
|
4
27
|
};
|
|
28
|
+
/**
|
|
29
|
+
* How long the edit-mode cookie's "true"/"false" values last before
|
|
30
|
+
* reverting to the session-derived default. Short on purpose: it only ever
|
|
31
|
+
* needs to outlive the handful of clicks between landing on ?edit=true and
|
|
32
|
+
* actually typing a password (or, for ?edit=false, a quick chrome-free
|
|
33
|
+
* preview) — not a whole working session, which persists through real auth
|
|
34
|
+
* instead. Bounds how long a stray copy of this cookie can show anything to
|
|
35
|
+
* whoever's browser it's left in, self-healing without needing an explicit
|
|
36
|
+
* logout.
|
|
37
|
+
*/
|
|
38
|
+
declare const GR_EDIT_MODE_TTL_SECONDS: number;
|
|
39
|
+
/**
|
|
40
|
+
* How long the set-a-password prompt stays up after an invite/recovery link
|
|
41
|
+
* completes. Longer than the edit-mode window above — picking a password
|
|
42
|
+
* takes more than one click — but still bounded, rather than nagging
|
|
43
|
+
* indefinitely if it's skipped and the cookie is somehow never cleared.
|
|
44
|
+
*/
|
|
45
|
+
declare const GR_NEEDS_PASSWORD_TTL_SECONDS: number;
|
|
5
46
|
/**
|
|
6
47
|
* Dedicated Postgres schema, not `public`. A name prefix alone only guards
|
|
7
48
|
* against accidental collisions; it does nothing against a `supabase db
|
|
@@ -19,4 +60,4 @@ declare const GR_TABLE = "gr_content";
|
|
|
19
60
|
declare const GR_MEMBERS_TABLE = "gr_members";
|
|
20
61
|
declare const GR_REPEATER_TABLE = "gr_repeater_items";
|
|
21
62
|
|
|
22
|
-
export { GR_COOKIES, GR_MEMBERS_TABLE, GR_REPEATER_TABLE, GR_SCHEMA, GR_TABLE };
|
|
63
|
+
export { GR_COOKIES, GR_EDIT_MODE_TTL_SECONDS, GR_MEMBERS_TABLE, GR_NEEDS_PASSWORD_TTL_SECONDS, GR_REPEATER_TABLE, GR_SCHEMA, GR_TABLE };
|
package/dist/core/defs.js
CHANGED
|
@@ -1,14 +1,41 @@
|
|
|
1
1
|
const GR_COOKIES = {
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Signals intent to edit. Carries no authority on its own — every write
|
|
4
|
+
* still goes through a real, server-verified role check, independent of
|
|
5
|
+
* this — but it does decide whether the admin chrome (the sign-in bar, or
|
|
6
|
+
* the toolbar) renders at all, so it is not harmless either: a stale copy
|
|
7
|
+
* of it is a real, if low-stakes, exposure ("there's a CMS here") to
|
|
8
|
+
* whoever's browser it's sitting in next, not just its original visitor.
|
|
9
|
+
*
|
|
10
|
+
* Values: "true" (show chrome — the short-lived grace window below, for
|
|
11
|
+
* someone not yet authenticated to reach the sign-in form), "false"
|
|
12
|
+
* (explicitly suppressed, e.g. ?edit=false previewing without chrome while
|
|
13
|
+
* still signed in), or absent. Absent defers entirely to whether a real
|
|
14
|
+
* session exists (see withGuardrail) — an authenticated editor's chrome
|
|
15
|
+
* persists for as long as their actual session does, with no separate
|
|
16
|
+
* timer of its own to fall out of sync with it.
|
|
17
|
+
*/
|
|
18
|
+
EDIT_MODE: "gr_edit_mode",
|
|
19
|
+
/**
|
|
20
|
+
* Set server-side the moment a redirect-carried session (an invite or
|
|
21
|
+
* password-recovery link) completes, so the next render — which has no
|
|
22
|
+
* other way to know that just happened — prompts to set a password
|
|
23
|
+
* instead of showing the normal toolbar. Cleared once they do, or once
|
|
24
|
+
* they explicitly skip.
|
|
25
|
+
*/
|
|
26
|
+
NEEDS_PASSWORD: "gr_needs_password"
|
|
4
27
|
};
|
|
28
|
+
const GR_EDIT_MODE_TTL_SECONDS = 10 * 60;
|
|
29
|
+
const GR_NEEDS_PASSWORD_TTL_SECONDS = 30 * 60;
|
|
5
30
|
const GR_SCHEMA = "guardrail";
|
|
6
31
|
const GR_TABLE = "gr_content";
|
|
7
32
|
const GR_MEMBERS_TABLE = "gr_members";
|
|
8
33
|
const GR_REPEATER_TABLE = "gr_repeater_items";
|
|
9
34
|
export {
|
|
10
35
|
GR_COOKIES,
|
|
36
|
+
GR_EDIT_MODE_TTL_SECONDS,
|
|
11
37
|
GR_MEMBERS_TABLE,
|
|
38
|
+
GR_NEEDS_PASSWORD_TTL_SECONDS,
|
|
12
39
|
GR_REPEATER_TABLE,
|
|
13
40
|
GR_SCHEMA,
|
|
14
41
|
GR_TABLE
|
package/dist/core/defs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/defs.ts"],"sourcesContent":["export const GR_COOKIES = {\n
|
|
1
|
+
{"version":3,"sources":["../../src/core/defs.ts"],"sourcesContent":["export const GR_COOKIES = {\n /**\n * Signals intent to edit. Carries no authority on its own — every write\n * still goes through a real, server-verified role check, independent of\n * this — but it does decide whether the admin chrome (the sign-in bar, or\n * the toolbar) renders at all, so it is not harmless either: a stale copy\n * of it is a real, if low-stakes, exposure (\"there's a CMS here\") to\n * whoever's browser it's sitting in next, not just its original visitor.\n *\n * Values: \"true\" (show chrome — the short-lived grace window below, for\n * someone not yet authenticated to reach the sign-in form), \"false\"\n * (explicitly suppressed, e.g. ?edit=false previewing without chrome while\n * still signed in), or absent. Absent defers entirely to whether a real\n * session exists (see withGuardrail) — an authenticated editor's chrome\n * persists for as long as their actual session does, with no separate\n * timer of its own to fall out of sync with it.\n */\n EDIT_MODE: \"gr_edit_mode\",\n /**\n * Set server-side the moment a redirect-carried session (an invite or\n * password-recovery link) completes, so the next render — which has no\n * other way to know that just happened — prompts to set a password\n * instead of showing the normal toolbar. Cleared once they do, or once\n * they explicitly skip.\n */\n NEEDS_PASSWORD: \"gr_needs_password\",\n} as const;\n\n/**\n * How long the edit-mode cookie's \"true\"/\"false\" values last before\n * reverting to the session-derived default. Short on purpose: it only ever\n * needs to outlive the handful of clicks between landing on ?edit=true and\n * actually typing a password (or, for ?edit=false, a quick chrome-free\n * preview) — not a whole working session, which persists through real auth\n * instead. Bounds how long a stray copy of this cookie can show anything to\n * whoever's browser it's left in, self-healing without needing an explicit\n * logout.\n */\nexport const GR_EDIT_MODE_TTL_SECONDS = 10 * 60;\n\n/**\n * How long the set-a-password prompt stays up after an invite/recovery link\n * completes. Longer than the edit-mode window above — picking a password\n * takes more than one click — but still bounded, rather than nagging\n * indefinitely if it's skipped and the cookie is somehow never cleared.\n */\nexport const GR_NEEDS_PASSWORD_TTL_SECONDS = 30 * 60;\n\n/**\n * Dedicated Postgres schema, not `public`. A name prefix alone only guards\n * against accidental collisions; it does nothing against a `supabase db\n * reset`, a broad `DROP ... CASCADE`, or a migration tool that diffs and\n * \"reconciles\" everything in `public` as if it owns that namespace. A\n * separate schema keeps Guardrail's tables outside that blast radius —\n * the same reason auth.* and storage.* are their own schemas, not public.*.\n *\n * Requires \"guardrail\" to be added under Settings > API > Exposed schemas in\n * the Supabase dashboard — PostgREST only serves schemas on that list, and no\n * migration can add itself to it.\n */\nexport const GR_SCHEMA = \"guardrail\";\n\nexport const GR_TABLE = \"gr_content\";\nexport const GR_MEMBERS_TABLE = \"gr_members\";\nexport const GR_REPEATER_TABLE = \"gr_repeater_items\";\n"],"mappings":"AAAO,MAAM,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBxB,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQX,gBAAgB;AAClB;AAYO,MAAM,2BAA2B,KAAK;AAQtC,MAAM,gCAAgC,KAAK;AAc3C,MAAM,YAAY;AAElB,MAAM,WAAW;AACjB,MAAM,mBAAmB;AACzB,MAAM,oBAAoB;","names":[]}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -113,6 +113,9 @@ interface GRAuthCapabilities {
|
|
|
113
113
|
interface GRState {
|
|
114
114
|
isAuth: boolean;
|
|
115
115
|
isEditMode: boolean;
|
|
116
|
+
/** A redirect-carried session (an invite or recovery link) just completed
|
|
117
|
+
* and has no password yet — see completeAuthRedirect. */
|
|
118
|
+
needsPassword: boolean;
|
|
116
119
|
siteId: string;
|
|
117
120
|
/** Null when the signed-in user holds no role on this site. */
|
|
118
121
|
role: GRRole | null;
|
|
@@ -145,5 +148,9 @@ type ActionState = {
|
|
|
145
148
|
email?: string;
|
|
146
149
|
password?: string;
|
|
147
150
|
};
|
|
151
|
+
type SetPasswordState = {
|
|
152
|
+
success?: boolean;
|
|
153
|
+
error?: string;
|
|
154
|
+
};
|
|
148
155
|
|
|
149
|
-
export type { ActionState, GRAsset, GRAuthCapabilities, GRContent, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, NextImageExtras, OverrideRow, OverrideType, OverrideValue, PolymorphicProps };
|
|
156
|
+
export type { ActionState, GRAsset, GRAuthCapabilities, GRContent, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, NextImageExtras, OverrideRow, OverrideType, OverrideValue, PolymorphicProps, SetPasswordState };
|
package/dist/index.client.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export { GRail } from './react/GRail/universal.js';
|
|
2
2
|
export { useGRail } from './react/Provider.js';
|
|
3
|
-
export { createAssetUpload, inviteMember, listAssets, listTeam, removeMember, updateMemberRole } from './server/actions.js';
|
|
3
|
+
export { createAssetUpload, inviteMember, listAssets, listTeam, removeMember, setPasswordAction, skipSetPasswordAction, updateMemberRole } from './server/actions.js';
|
|
4
4
|
export { addRepeaterItem, removeRepeaterItem, reorderRepeaterItems } from './server/repeaterActions.js';
|
|
5
5
|
export { GRepeaterProps, RepeaterItemProps } from './react/Repeater/index.js';
|
|
6
|
-
export { GRAsset, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, OverrideRow, OverrideType, OverrideValue, PolymorphicProps } from './core/types.js';
|
|
7
|
-
export { AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
|
|
6
|
+
export { ActionState, GRAsset, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, OverrideRow, OverrideType, OverrideValue, PolymorphicProps, SetPasswordState } from './core/types.js';
|
|
7
|
+
export { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
|
|
8
8
|
import 'react/jsx-runtime';
|
|
9
9
|
import 'react';
|
|
10
10
|
|
package/dist/index.client.js
CHANGED
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
updateMemberRole,
|
|
7
7
|
removeMember,
|
|
8
8
|
createAssetUpload,
|
|
9
|
-
listAssets
|
|
9
|
+
listAssets,
|
|
10
|
+
setPasswordAction,
|
|
11
|
+
skipSetPasswordAction
|
|
10
12
|
} from "./server/actions";
|
|
11
13
|
import {
|
|
12
14
|
addRepeaterItem,
|
|
@@ -59,6 +61,8 @@ export {
|
|
|
59
61
|
removeMember,
|
|
60
62
|
removeRepeaterItem,
|
|
61
63
|
reorderRepeaterItems,
|
|
64
|
+
setPasswordAction,
|
|
65
|
+
skipSetPasswordAction,
|
|
62
66
|
updateMemberRole,
|
|
63
67
|
useGRail,
|
|
64
68
|
withGuardrail
|
package/dist/index.client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.client.ts"],"sourcesContent":["// Client-graph entry. `import ... from \"@guard-rail/core\"` resolves here from a\n// Client Component (see \"exports\" in package.json) and to ./index from a\n// Server Component, so an app uses one import path everywhere.\n//\n// Nothing server-only may be imported here — this is bundled for the browser.\n// Server Actions are fine: a \"use server\" module is replaced by references in\n// the client bundle, which is what lets an app build its own team/repeater UI\n// in a Client Component.\n\nexport { GRail } from \"./react/GRail/universal\";\nexport { useGRail } from \"./react/Provider\";\n\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\n} from \"./server/actions\";\nexport {\n addRepeaterItem,\n removeRepeaterItem,\n reorderRepeaterItems,\n} from \"./server/repeaterActions\";\n\n/**\n * Stand-ins for the server-only exports, so importing one from a Client\n * Component fails with an explanation instead of an undefined-is-not-a-function\n * deep in a render. Also what a tool that doesn't set the \"react-server\"\n * condition (a test runner, a plain Node script) gets from the package root —\n * `@guard-rail/core/server` is the explicit path to the real ones there.\n */\nfunction serverOnly(name: string, hint: string) {\n return (..._args: unknown[]): never => {\n throw new Error(\n `Guardrail: ${name} only works in a Server Component. ${hint} ` +\n `(If this isn't a Client Component, your tooling may not set the ` +\n `\"react-server\" condition — import from \"@guard-rail/core/server\" instead.)`,\n );\n };\n}\n\nexport const withGuardrail = serverOnly(\n \"withGuardrail()\",\n \"Call it in your root layout, which is a Server Component.\",\n);\nexport const configureGuardrail = serverOnly(\n \"configureGuardrail()\",\n \"Configuration is applied by withGuardrail() in your root layout.\",\n);\nexport const GRepeater = serverOnly(\n \"<GRepeater>\",\n \"Render it in a Server Component and pass it into this one as children or a prop.\",\n);\nexport const listRepeaterItems = serverOnly(\n \"listRepeaterItems()\",\n \"Read repeater items in a Server Component and pass them down.\",\n);\nexport const getRepeaterItemBySlug = serverOnly(\n \"getRepeaterItemBySlug()\",\n \"Resolve the item in a Server Component (your [slug]/page.tsx) and pass it down.\",\n);\nexport const getFieldValue = serverOnly(\n \"getFieldValue()\",\n \"Read the value in a Server Component and pass it down as a prop.\",\n);\n\nexport type { GRepeaterProps, RepeaterItemProps } from \"./react/Repeater\";\nexport type {\n GRState,\n GRRole,\n GRMember,\n GRViewer,\n GRAsset,\n GRRepeaterItem,\n OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRPrefetch,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n} from \"./core/adapter-types\";\n"],"mappings":"AASA,SAAS,aAAa;AACtB,SAAS,gBAAgB;AAEzB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP,SAAS,WAAW,MAAc,MAAc;AAC9C,SAAO,IAAI,UAA4B;AACrC,UAAM,IAAI;AAAA,MACR,cAAc,IAAI,sCAAsC,IAAI;AAAA,IAG9D;AAAA,EACF;AACF;AAEO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;AACO,MAAM,qBAAqB;AAAA,EAChC;AAAA,EACA;AACF;AACO,MAAM,YAAY;AAAA,EACvB;AAAA,EACA;AACF;AACO,MAAM,oBAAoB;AAAA,EAC/B;AAAA,EACA;AACF;AACO,MAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AACF;AACO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.client.ts"],"sourcesContent":["// Client-graph entry. `import ... from \"@guard-rail/core\"` resolves here from a\n// Client Component (see \"exports\" in package.json) and to ./index from a\n// Server Component, so an app uses one import path everywhere.\n//\n// Nothing server-only may be imported here — this is bundled for the browser.\n// Server Actions are fine: a \"use server\" module is replaced by references in\n// the client bundle, which is what lets an app build its own team/repeater UI\n// in a Client Component.\n\nexport { GRail } from \"./react/GRail/universal\";\nexport { useGRail } from \"./react/Provider\";\n\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\n setPasswordAction,\n skipSetPasswordAction,\n} from \"./server/actions\";\nexport {\n addRepeaterItem,\n removeRepeaterItem,\n reorderRepeaterItems,\n} from \"./server/repeaterActions\";\n\n/**\n * Stand-ins for the server-only exports, so importing one from a Client\n * Component fails with an explanation instead of an undefined-is-not-a-function\n * deep in a render. Also what a tool that doesn't set the \"react-server\"\n * condition (a test runner, a plain Node script) gets from the package root —\n * `@guard-rail/core/server` is the explicit path to the real ones there.\n */\nfunction serverOnly(name: string, hint: string) {\n return (..._args: unknown[]): never => {\n throw new Error(\n `Guardrail: ${name} only works in a Server Component. ${hint} ` +\n `(If this isn't a Client Component, your tooling may not set the ` +\n `\"react-server\" condition — import from \"@guard-rail/core/server\" instead.)`,\n );\n };\n}\n\nexport const withGuardrail = serverOnly(\n \"withGuardrail()\",\n \"Call it in your root layout, which is a Server Component.\",\n);\nexport const configureGuardrail = serverOnly(\n \"configureGuardrail()\",\n \"Configuration is applied by withGuardrail() in your root layout.\",\n);\nexport const GRepeater = serverOnly(\n \"<GRepeater>\",\n \"Render it in a Server Component and pass it into this one as children or a prop.\",\n);\nexport const listRepeaterItems = serverOnly(\n \"listRepeaterItems()\",\n \"Read repeater items in a Server Component and pass them down.\",\n);\nexport const getRepeaterItemBySlug = serverOnly(\n \"getRepeaterItemBySlug()\",\n \"Resolve the item in a Server Component (your [slug]/page.tsx) and pass it down.\",\n);\nexport const getFieldValue = serverOnly(\n \"getFieldValue()\",\n \"Read the value in a Server Component and pass it down as a prop.\",\n);\n\nexport type { GRepeaterProps, RepeaterItemProps } from \"./react/Repeater\";\nexport type {\n GRState,\n GRRole,\n GRMember,\n GRViewer,\n GRAsset,\n GRRepeaterItem,\n ActionState,\n SetPasswordState,\n OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRPrefetch,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n AuthRedirectParams,\n} from \"./core/adapter-types\";\n"],"mappings":"AASA,SAAS,aAAa;AACtB,SAAS,gBAAgB;AAEzB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP,SAAS,WAAW,MAAc,MAAc;AAC9C,SAAO,IAAI,UAA4B;AACrC,UAAM,IAAI;AAAA,MACR,cAAc,IAAI,sCAAsC,IAAI;AAAA,IAG9D;AAAA,EACF;AACF;AAEO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;AACO,MAAM,qBAAqB;AAAA,EAChC;AAAA,EACA;AACF;AACO,MAAM,YAAY;AAAA,EACvB;AAAA,EACA;AACF;AACO,MAAM,oBAAoB;AAAA,EAC/B;AAAA,EACA;AACF;AACO,MAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AACF;AACO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { withGuardrail } from './server/wrapper.js';
|
|
2
|
-
export { createAssetUpload, inviteMember, listAssets, listTeam, removeMember, updateMemberRole } from './server/actions.js';
|
|
2
|
+
export { createAssetUpload, inviteMember, listAssets, listTeam, removeMember, setPasswordAction, skipSetPasswordAction, updateMemberRole } from './server/actions.js';
|
|
3
3
|
export { GRail } from './react/GRail/index.js';
|
|
4
4
|
export { GRepeater, GRepeaterProps, RepeaterItemProps } from './react/Repeater/index.js';
|
|
5
5
|
export { configureGuardrail } from './core/config.js';
|
|
@@ -7,7 +7,7 @@ export { getRepeaterItemBySlug, listRepeaterItems } from './server/repeaterData.
|
|
|
7
7
|
export { getFieldValue } from './server/fieldData.js';
|
|
8
8
|
export { addRepeaterItem, removeRepeaterItem, reorderRepeaterItems } from './server/repeaterActions.js';
|
|
9
9
|
export { useGRail } from './react/Provider.js';
|
|
10
|
-
export { GRAsset, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, OverrideRow, OverrideType, OverrideValue, PolymorphicProps } from './core/types.js';
|
|
11
|
-
export { AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
|
|
10
|
+
export { ActionState, GRAsset, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, OverrideRow, OverrideType, OverrideValue, PolymorphicProps, SetPasswordState } from './core/types.js';
|
|
11
|
+
export { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
|
|
12
12
|
import 'react/jsx-runtime';
|
|
13
13
|
import 'react';
|