create-astroid 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/index.mjs +42 -2
- package/package.json +4 -4
- package/template/src/pages/login.astro +42 -32
package/README.md
CHANGED
|
@@ -38,6 +38,9 @@ pnpm create astroid [directory] [options]
|
|
|
38
38
|
--host <domain> Primary domain, e.g. example.com
|
|
39
39
|
--commerce <provider> square | stripe | fourthwall — also adds the queue
|
|
40
40
|
consumer, webhook receiver, and cron safety net
|
|
41
|
+
--square-locations <n> single | multi — with --commerce square; multi is one
|
|
42
|
+
Square Location per merchant, resolved from the request
|
|
43
|
+
host rather than SQUARE_LOCATION_ID
|
|
41
44
|
--map Self-hosted PMTiles/MapLibre location map
|
|
42
45
|
--pwa Installable PWA: a scoped service worker that never
|
|
43
46
|
caches /api/* or the editor, plus a manifest
|
package/index.mjs
CHANGED
|
@@ -57,6 +57,11 @@ const ARCHETYPES = Object.keys(ARCHETYPE_SECTIONS);
|
|
|
57
57
|
// consumer, the webhook receiver, and the cron safety net.
|
|
58
58
|
const COMMERCE_PROVIDERS = ["square", "stripe", "fourthwall"];
|
|
59
59
|
|
|
60
|
+
// Square's location model (`commerce.square.locations`). `multi` is the
|
|
61
|
+
// multi-merchant shape: the location comes from the request, not the
|
|
62
|
+
// environment, and the checkout route is scaffolded around resolving it.
|
|
63
|
+
const SQUARE_LOCATIONS = ["single", "multi"];
|
|
64
|
+
|
|
60
65
|
// --- args ------------------------------------------------------------------
|
|
61
66
|
function parseArgs(argv) {
|
|
62
67
|
const flags = {};
|
|
@@ -189,8 +194,17 @@ function astroidConfigSource(config) {
|
|
|
189
194
|
` colors: { brand: ${JSON.stringify(config.theme.colors.brand)} },`,
|
|
190
195
|
" },",
|
|
191
196
|
` sections: ${JSON.stringify(config.sections)},`,
|
|
197
|
+
// `square` must be emitted too: `astroid doctor` derives the required
|
|
198
|
+
// secrets from THIS file, so a multi-location project whose config lost the
|
|
199
|
+
// option would be told it is missing a SQUARE_LOCATION_ID it must not have.
|
|
192
200
|
...(config.commerce
|
|
193
|
-
? [
|
|
201
|
+
? [
|
|
202
|
+
` commerce: { provider: ${JSON.stringify(config.commerce.provider)}${
|
|
203
|
+
config.commerce.square?.locations
|
|
204
|
+
? `, square: { locations: ${JSON.stringify(config.commerce.square.locations)} }`
|
|
205
|
+
: ""
|
|
206
|
+
} },`,
|
|
207
|
+
]
|
|
194
208
|
: []),
|
|
195
209
|
// Must be emitted, for the same reason the portal is: `astroid generate`
|
|
196
210
|
// rebuilds the middleware and CSP from THIS file, so a config that dropped
|
|
@@ -244,6 +258,9 @@ Options:
|
|
|
244
258
|
--host <domain> Primary domain, e.g. example.com
|
|
245
259
|
--commerce <provider> ${COMMERCE_PROVIDERS.join(" | ")}
|
|
246
260
|
Also adds the queue consumer, webhook receiver, and cron
|
|
261
|
+
--square-locations <n> ${SQUARE_LOCATIONS.join(" | ")} (default: single; needs --commerce square)
|
|
262
|
+
multi: one Square Location per merchant, resolved from
|
|
263
|
+
the request host — no SQUARE_LOCATION_ID
|
|
247
264
|
--map Add the self-hosted PMTiles/MapLibre location map
|
|
248
265
|
--pwa Add an installable PWA: a scoped service worker that
|
|
249
266
|
never caches /api/* or the editor, plus a manifest
|
|
@@ -317,6 +334,22 @@ async function main() {
|
|
|
317
334
|
);
|
|
318
335
|
process.exit(1);
|
|
319
336
|
}
|
|
337
|
+
// Refused, not ignored, without Square: the checkout route is scaffolded ONCE,
|
|
338
|
+
// so a multi-merchant store that silently got the single-location route would
|
|
339
|
+
// ring every sale against one ambient SQUARE_LOCATION_ID — and look fine doing it.
|
|
340
|
+
const squareLocationsRaw = flags["square-locations"];
|
|
341
|
+
const squareLocations =
|
|
342
|
+
typeof squareLocationsRaw === "string" ? squareLocationsRaw.toLowerCase() : undefined;
|
|
343
|
+
if (squareLocationsRaw !== undefined && !SQUARE_LOCATIONS.includes(squareLocations)) {
|
|
344
|
+
process.stderr.write(
|
|
345
|
+
`create-astroid: --square-locations expects ${SQUARE_LOCATIONS.join(" | ")}\n`,
|
|
346
|
+
);
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
if (squareLocations && commerce !== "square") {
|
|
350
|
+
process.stderr.write("create-astroid: --square-locations needs --commerce square\n");
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
320
353
|
|
|
321
354
|
if (existsSync(dir) && readdirSync(dir).length > 0) {
|
|
322
355
|
process.stderr.write(`create-astroid: target directory is not empty: ${dir}\n`);
|
|
@@ -330,7 +363,14 @@ async function main() {
|
|
|
330
363
|
...(host ? { hosts: [host] } : {}),
|
|
331
364
|
theme: { name, colors: { brand: color } },
|
|
332
365
|
sections: ARCHETYPE_SECTIONS[archetype],
|
|
333
|
-
...(commerce
|
|
366
|
+
...(commerce
|
|
367
|
+
? {
|
|
368
|
+
commerce: {
|
|
369
|
+
provider: commerce,
|
|
370
|
+
...(squareLocations ? { square: { locations: squareLocations } } : {}),
|
|
371
|
+
},
|
|
372
|
+
}
|
|
373
|
+
: {}),
|
|
334
374
|
// Unprefixed `user`/`session` (customers — email + password), so the studio's
|
|
335
375
|
// `louise_`-prefixed tables and the portal's never collide (mirrors the
|
|
336
376
|
// reference storefront). `signUp: true` because a shop lets customers register.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-astroid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Scaffold a new Astroid site — an editable, multi-editor Astro app on Cloudflare Workers — in one command.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@better-auth/passkey": "^1.7.2",
|
|
34
|
-
"@louise-toolkit/astro": "^0.
|
|
34
|
+
"@louise-toolkit/astro": "^0.2.1",
|
|
35
35
|
"better-auth": "^1.7.2",
|
|
36
|
-
"louise-toolkit": "^0.
|
|
37
|
-
"astroidjs": "0.
|
|
36
|
+
"louise-toolkit": "^0.30.1",
|
|
37
|
+
"astroidjs": "0.12.0"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
|
40
40
|
"node": ">=26.0.0"
|
|
@@ -45,9 +45,9 @@ const siteKey = turnstileSiteKey(env);
|
|
|
45
45
|
</label>
|
|
46
46
|
{
|
|
47
47
|
siteKey && (
|
|
48
|
-
/*
|
|
49
|
-
|
|
50
|
-
<div
|
|
48
|
+
/* Rendered explicitly by the script below, which reads the key
|
|
49
|
+
from here — a processed script can't take `define:vars`. */
|
|
50
|
+
<div id="captcha" data-sitekey={siteKey} />
|
|
51
51
|
)
|
|
52
52
|
}
|
|
53
53
|
<button type="submit" class="btn btn-primary">Email me a sign-in link</button>
|
|
@@ -57,43 +57,45 @@ const siteKey = turnstileSiteKey(env);
|
|
|
57
57
|
</div>
|
|
58
58
|
</main>
|
|
59
59
|
|
|
60
|
-
{
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
siteKey && (
|
|
65
|
-
<script
|
|
66
|
-
is:inline
|
|
67
|
-
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
|
|
68
|
-
async
|
|
69
|
-
defer
|
|
70
|
-
/>
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
{/* Processed (NOT is:inline) so Astro's security.csp hashes it into script-src.
|
|
75
|
-
Keep it dependency-free and static — an is:inline or define:vars script here
|
|
76
|
-
would be unhashable and CSP-blocked. */}
|
|
60
|
+
{/* Processed (NOT is:inline) so Astro's security.csp hashes it into script-src,
|
|
61
|
+
and bundles the import. The widget's own script is loaded by
|
|
62
|
+
`renderTurnstile` from challenges.cloudflare.com, which the CSP already
|
|
63
|
+
allows for script, frame and connect (astroidSecurity). */}
|
|
77
64
|
<script>
|
|
78
|
-
|
|
65
|
+
import { renderTurnstile, type TurnstileWidget } from "louise-toolkit/forms/turnstile";
|
|
66
|
+
|
|
67
|
+
const form = document.getElementById("magic") as HTMLFormElement | null;
|
|
79
68
|
const msg = document.getElementById("msg");
|
|
69
|
+
const captcha = document.getElementById("captcha");
|
|
70
|
+
|
|
71
|
+
// Absent when no real site key is set — then the server's captcha plugin is
|
|
72
|
+
// off too, and the request goes without a token.
|
|
73
|
+
let widget: TurnstileWidget | null = null;
|
|
74
|
+
const ready = captcha?.dataset.sitekey
|
|
75
|
+
? renderTurnstile(captcha, { siteKey: captcha.dataset.sitekey, size: "flexible" })
|
|
76
|
+
.then((w) => {
|
|
77
|
+
widget = w;
|
|
78
|
+
})
|
|
79
|
+
.catch(() => {
|
|
80
|
+
// The server will refuse a request without a token, so say so now
|
|
81
|
+
// rather than after the owner has typed their email.
|
|
82
|
+
if (msg) msg.textContent = "The sign-in check didn't load — reload the page to try again.";
|
|
83
|
+
})
|
|
84
|
+
: Promise.resolve();
|
|
85
|
+
|
|
80
86
|
form?.addEventListener("submit", async (e) => {
|
|
81
87
|
e.preventDefault();
|
|
82
88
|
const input = document.getElementById("email") as HTMLInputElement | null;
|
|
83
89
|
const email = input?.value.trim();
|
|
84
90
|
if (!email || !msg) return;
|
|
91
|
+
await ready;
|
|
92
|
+
if (captcha && !widget) return; // the load failure is already on screen
|
|
85
93
|
msg.textContent = "Sending…";
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
// missing — so this header is what makes a captcha-enabled sign-in work.
|
|
90
|
-
// Absent when no widget rendered, in which case the plugin isn't active
|
|
91
|
-
// either and the header is simply not sent.
|
|
92
|
-
const token = (form as HTMLFormElement).querySelector<HTMLInputElement>(
|
|
93
|
-
'input[name="cf-turnstile-response"]',
|
|
94
|
-
)?.value;
|
|
94
|
+
// Better Auth's captcha plugin reads the token from the `x-captcha-response`
|
|
95
|
+
// HEADER (not the body), and rejects the request outright when it's missing.
|
|
96
|
+
const token = widget?.token();
|
|
95
97
|
try {
|
|
96
|
-
await fetch("/api/auth/sign-in/magic-link", {
|
|
98
|
+
const res = await fetch("/api/auth/sign-in/magic-link", {
|
|
97
99
|
method: "POST",
|
|
98
100
|
headers: {
|
|
99
101
|
"content-type": "application/json",
|
|
@@ -101,9 +103,17 @@ const siteKey = turnstileSiteKey(env);
|
|
|
101
103
|
},
|
|
102
104
|
body: JSON.stringify({ email, callbackURL: "/?louise" }),
|
|
103
105
|
});
|
|
104
|
-
|
|
106
|
+
// Enumeration-safe either way: a non-editor gets the same 200 as an
|
|
107
|
+
// editor, so a failure here is the captcha, a rate limit, or an outage.
|
|
108
|
+
msg.textContent = res.ok
|
|
109
|
+
? "If that email is an editor, a sign-in link is on its way."
|
|
110
|
+
: "That didn't go through — please try again.";
|
|
105
111
|
} catch {
|
|
106
112
|
msg.textContent = "Something went wrong — please try again.";
|
|
113
|
+
} finally {
|
|
114
|
+
// A token is single-use, and this one is spent whatever happened. Without
|
|
115
|
+
// a fresh one, "send it again" posts the spent token and is refused.
|
|
116
|
+
widget?.reset();
|
|
107
117
|
}
|
|
108
118
|
});
|
|
109
119
|
</script>
|