astroidjs 0.12.1 → 0.14.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 +42 -42
- package/bin/astroid.mjs +35 -29
- package/dist/analytics/index.d.ts +3 -3
- package/dist/analytics/index.js +11 -11
- package/dist/astro/csp.d.ts +5 -5
- package/dist/astro/csp.js +6 -6
- package/dist/astro/index.js +1 -1
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/index.js +5 -5
- package/dist/commerce/adapters.d.ts +6 -6
- package/dist/commerce/adapters.js +9 -9
- package/dist/commerce/checkout-scaffold.d.ts +5 -5
- package/dist/commerce/checkout-scaffold.js +27 -27
- package/dist/commerce/checkout.d.ts +12 -12
- package/dist/commerce/checkout.js +7 -7
- package/dist/commerce/loader.d.ts +2 -2
- package/dist/commerce/loader.js +3 -3
- package/dist/commerce/mirror.d.ts +4 -4
- package/dist/commerce/mirror.js +12 -12
- package/dist/commerce/roles.d.ts +10 -10
- package/dist/commerce/roles.js +13 -13
- package/dist/commerce/secrets.d.ts +9 -9
- package/dist/commerce/secrets.js +9 -9
- package/dist/commerce/sync.d.ts +7 -7
- package/dist/commerce/sync.js +5 -5
- package/dist/components/sections.d.ts +9 -9
- package/dist/components/sections.js +12 -12
- package/dist/config.d.ts +89 -62
- package/dist/config.js +18 -18
- package/dist/email/inquiry.d.ts +2 -2
- package/dist/email/inquiry.js +1 -1
- package/dist/email/send.d.ts +4 -4
- package/dist/email/send.js +7 -7
- package/dist/email/templates.js +3 -3
- package/dist/email/theme.d.ts +1 -1
- package/dist/email/theme.js +4 -4
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/index.js +1 -1
- package/dist/map/pmtiles.d.ts +5 -5
- package/dist/map/pmtiles.js +5 -5
- package/dist/map/scaffold.d.ts +2 -2
- package/dist/map/scaffold.js +8 -8
- package/dist/map/style.d.ts +4 -4
- package/dist/map/style.js +1 -1
- package/dist/portal/config.d.ts +3 -3
- package/dist/portal/config.js +11 -10
- package/dist/portal/guard.d.ts +4 -4
- package/dist/portal/guard.js +4 -4
- package/dist/portal/nav.js +2 -2
- package/dist/portal/scaffold.d.ts +4 -4
- package/dist/portal/scaffold.js +13 -13
- package/dist/portal/session.d.ts +11 -5
- package/dist/portal/session.js +10 -5
- package/dist/portfolio/scaffold.d.ts +1 -1
- package/dist/portfolio/scaffold.js +8 -8
- package/dist/project/actions.d.ts +1 -1
- package/dist/project/actions.js +18 -12
- package/dist/project/generate.d.ts +4 -4
- package/dist/project/generate.js +26 -26
- package/dist/project/index.d.ts +1 -0
- package/dist/project/index.js +2 -1
- package/dist/project/scaffold.d.ts +2 -2
- package/dist/project/scaffold.js +69 -13
- package/dist/project/seed.d.ts +22 -0
- package/dist/project/seed.js +214 -0
- package/dist/pwa/generate.d.ts +11 -11
- package/dist/pwa/generate.js +19 -19
- package/dist/queues/consumer.d.ts +3 -3
- package/dist/queues/consumer.js +2 -2
- package/dist/queues/messages.d.ts +4 -4
- package/dist/queues/messages.js +2 -2
- package/dist/queues/scaffold.d.ts +4 -4
- package/dist/queues/scaffold.js +17 -17
- package/dist/queues/webhook.d.ts +5 -5
- package/dist/queues/webhook.js +3 -3
- package/dist/realtime/scaffold.d.ts +4 -4
- package/dist/realtime/scaffold.js +12 -12
- package/dist/schema/collections.d.ts +42 -11
- package/dist/schema/collections.js +43 -42
- package/dist/schema/framework.d.ts +1 -1
- package/dist/schema/framework.js +2 -2
- package/dist/schema/generate.js +6 -6
- package/dist/schema/index.js +1 -1
- package/dist/secrets.d.ts +6 -6
- package/dist/secrets.js +6 -6
- package/dist/security/csp-origins.d.ts +1 -1
- package/dist/security/csp-origins.js +3 -3
- package/dist/security/rate-rules.d.ts +2 -2
- package/dist/security/rate-rules.js +8 -8
- package/dist/seo/resolve.d.ts +5 -5
- package/dist/seo/resolve.js +2 -2
- package/dist/seo/routes.d.ts +5 -5
- package/dist/seo/routes.js +3 -3
- package/dist/seo/structured-data.d.ts +6 -6
- package/dist/seo/structured-data.js +7 -7
- package/dist/status.d.ts +5 -5
- package/dist/status.js +7 -7
- package/dist/tenancy/index.d.ts +3 -3
- package/dist/tenancy/index.js +11 -11
- package/dist/worker/generate.d.ts +2 -2
- package/dist/worker/generate.js +91 -57
- package/dist/worker/index.js +1 -1
- package/dist/worker/routes.js +11 -11
- package/dist/workflow/advance.d.ts +3 -3
- package/dist/workflow/advance.js +6 -6
- package/dist/workflow/config.d.ts +4 -4
- package/dist/workflow/config.js +4 -4
- package/dist/workflow/generate.d.ts +2 -2
- package/dist/workflow/generate.js +11 -11
- package/package.json +3 -4
- package/src/components/Collection.tsx +5 -5
- package/src/components/Editable.astro +9 -9
- package/src/components/JustifiedGallery.astro +8 -8
- package/src/components/MediaSlot.astro +12 -12
- package/src/components/PortalShell.astro +4 -4
- package/src/components/RegisterSW.astro +3 -3
- package/src/components/Section.astro +8 -8
- package/src/components/Sections.astro +6 -6
- package/src/components/Seo.astro +3 -3
- package/src/components/StageBar.astro +3 -3
- package/src/components/StructuredData.astro +2 -2
- package/src/components/justify.ts +9 -9
- package/src/components/media-meta.ts +10 -10
- package/src/components/sections/AboutIntro.astro +1 -1
- package/src/components/sections/Contact.astro +1 -1
- package/src/components/sections/Cta.astro +1 -1
- package/src/components/sections/Faq.astro +1 -1
- package/src/components/sections/FeatureGrid.astro +2 -2
- package/src/components/sections/Hero.astro +1 -1
- package/src/components/sections/PricingTiers.astro +1 -1
- package/src/components/sections/ProductGrid.astro +1 -1
- package/src/components/sections/SplitImage.astro +1 -1
- package/src/components/sections/Steps.astro +1 -1
- package/src/components/sections/Testimonial.astro +1 -1
- package/src/components/sections.ts +17 -17
package/dist/astro/csp.js
CHANGED
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
// - **Astro owns `script-src`.** Its `security.csp` hashes every script it
|
|
10
10
|
// processes, so the policy can be `'self'` with no `'unsafe-inline'`. What it
|
|
11
11
|
// does NOT hash is Solid's hydration bootstrap, which `@astrojs/solid-js`
|
|
12
|
-
// injects on every page carrying an island
|
|
12
|
+
// injects on every page carrying an island—Astro only tracks its own inline
|
|
13
13
|
// scripts. So we compute that hash from the very function the renderer calls,
|
|
14
14
|
// which means it follows solid-js upgrades instead of going stale as a
|
|
15
15
|
// copy-pasted literal.
|
|
16
16
|
// - **The middleware owns `style-src`.** Louise's data-driven `style=""`
|
|
17
17
|
// carriers and the editor's runtime-injected `<style>` need
|
|
18
18
|
// `'unsafe-inline'`, and per spec a single hash in `style-src` VOIDS
|
|
19
|
-
// `'unsafe-inline'
|
|
19
|
+
// `'unsafe-inline'`—so the two cannot coexist in one directive. The
|
|
20
20
|
// generated middleware rewrites that one directive after the fact
|
|
21
21
|
// (`cspStyleSrc`), leaving Astro's script hashes verbatim.
|
|
22
22
|
//
|
|
@@ -33,7 +33,7 @@ export { astroidCspOrigins } from "../security/csp-origins.js";
|
|
|
33
33
|
* Hash of Solid's inline hydration bootstrap.
|
|
34
34
|
*
|
|
35
35
|
* `@astrojs/solid-js` injects this script on every page with an island, but
|
|
36
|
-
* Astro's CSP tracker only hashes scripts it processed itself
|
|
36
|
+
* Astro's CSP tracker only hashes scripts it processed itself—so without this
|
|
37
37
|
* the bootstrap is blocked under `script-src 'self'` and every island silently
|
|
38
38
|
* fails to hydrate. Computed from `generateHydrationScript()` (the same call the
|
|
39
39
|
* renderer makes), so a solid-js upgrade that changes the bootstrap updates the
|
|
@@ -45,7 +45,7 @@ export function solidHydrationHash() {
|
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
47
|
* Render one directive. The name is a literal from the union above, so the
|
|
48
|
-
* concatenation is a valid `CspDirective` by construction
|
|
48
|
+
* concatenation is a valid `CspDirective` by construction—which is what the
|
|
49
49
|
* assertion is standing in for (TS widens template concatenation to `string`).
|
|
50
50
|
*/
|
|
51
51
|
function directive(name, ...sources) {
|
|
@@ -53,7 +53,7 @@ function directive(name, ...sources) {
|
|
|
53
53
|
return (list ? `${name} ${list}` : name);
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
|
-
* The `security` block for `astro.config.mjs
|
|
56
|
+
* The `security` block for `astro.config.mjs`—Astro's half of the split.
|
|
57
57
|
*
|
|
58
58
|
* `style-src` is deliberately absent: the generated middleware rewrites it per
|
|
59
59
|
* response, and declaring it here would be the hash-vs-`'unsafe-inline'`
|
|
@@ -93,7 +93,7 @@ export function astroidSecurity(config) {
|
|
|
93
93
|
}
|
|
94
94
|
/**
|
|
95
95
|
* Vite build options the CSP depends on. `assetsInlineLimit: 0` stops Vite from
|
|
96
|
-
* inlining small assets as `data:` URLs
|
|
96
|
+
* inlining small assets as `data:` URLs—an inlined script would be inline, and
|
|
97
97
|
* therefore unhashed, and therefore blocked by `script-src 'self'`. Spread this
|
|
98
98
|
* into `vite.build` rather than remembering why the number is zero.
|
|
99
99
|
*/
|
package/dist/astro/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
2
|
//
|
|
3
|
-
// `astroidjs/astro
|
|
3
|
+
// `astroidjs/astro`—the BUILD-TIME surface, imported from `astro.config.mjs`.
|
|
4
4
|
// Kept off the main entry on purpose: it reaches for `node:crypto` and
|
|
5
5
|
// `solid-js/web`, neither of which belongs in the Worker bundle the generated
|
|
6
6
|
// `worker.ts` produces.
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AstroidConfig } from "../config.js";
|
|
2
2
|
/**
|
|
3
|
-
* The editor Better Auth instance's table prefix
|
|
3
|
+
* The editor Better Auth instance's table prefix—`louise_user`,
|
|
4
4
|
* `louise_session`, … The unprefixed names are left free for a second (portal)
|
|
5
5
|
* instance. Consumed by the generated `editorsRoute` and mirrored by the
|
|
6
6
|
* scaffolded `src/auth.ts` (`getLouiseAuth({ tablePrefix })`) + its migration.
|
|
@@ -12,7 +12,7 @@ export declare const ASTROID_EDITOR_TABLE_PREFIX = "louise_";
|
|
|
12
12
|
* reject a portal that would collide with it.
|
|
13
13
|
*/
|
|
14
14
|
export declare const ASTROID_EDITOR_COOKIE_PREFIX = "better-auth";
|
|
15
|
-
/** The editor's Better Auth table name for a given model (
|
|
15
|
+
/** The editor's Better Auth table name for a given model (for example, `louise_user`). */
|
|
16
16
|
export declare function astroidEditorTable(model: string): string;
|
|
17
17
|
/**
|
|
18
18
|
* Reject a portal whose isolation would collide with the editor instance on the
|
package/dist/auth/index.js
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
// Editor-auth convention + the two-instance isolation guard.
|
|
4
4
|
//
|
|
5
5
|
// Astroid runs up to TWO Better Auth instances on one origin: the EDITOR (the
|
|
6
|
-
// studio
|
|
7
|
-
// second PORTAL instance (customers/members/a shop account
|
|
6
|
+
// studio—magic-link + passkey, a DB-managed admin allowlist) and, optionally, a
|
|
7
|
+
// second PORTAL instance (customers/members/a shop account—see `portal/`). The
|
|
8
8
|
// editor owns Better Auth's default mount (`/api/auth`) and cookie because the
|
|
9
9
|
// Louise editor client hardcodes them; its tables are namespaced with the
|
|
10
10
|
// `louise_` prefix so a second instance can take the unprefixed `user`/`session`
|
|
11
|
-
// tables without collision. The portal is the one that MOVES
|
|
11
|
+
// tables without collision. The portal is the one that MOVES—a distinct mount,
|
|
12
12
|
// cookie prefix, and (by default) table prefix.
|
|
13
13
|
//
|
|
14
14
|
// The failure this guards against is subtle and intermittent: two instances that
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { AstroidConfigError } from "../errors.js";
|
|
19
19
|
import { astroidPortal } from "../portal/config.js";
|
|
20
20
|
/**
|
|
21
|
-
* The editor Better Auth instance's table prefix
|
|
21
|
+
* The editor Better Auth instance's table prefix—`louise_user`,
|
|
22
22
|
* `louise_session`, … The unprefixed names are left free for a second (portal)
|
|
23
23
|
* instance. Consumed by the generated `editorsRoute` and mirrored by the
|
|
24
24
|
* scaffolded `src/auth.ts` (`getLouiseAuth({ tablePrefix })`) + its migration.
|
|
@@ -30,7 +30,7 @@ export const ASTROID_EDITOR_TABLE_PREFIX = "louise_";
|
|
|
30
30
|
* reject a portal that would collide with it.
|
|
31
31
|
*/
|
|
32
32
|
export const ASTROID_EDITOR_COOKIE_PREFIX = "better-auth";
|
|
33
|
-
/** The editor's Better Auth table name for a given model (
|
|
33
|
+
/** The editor's Better Auth table name for a given model (for example, `louise_user`). */
|
|
34
34
|
export function astroidEditorTable(model) {
|
|
35
35
|
return `${ASTROID_EDITOR_TABLE_PREFIX}${model}`;
|
|
36
36
|
}
|
|
@@ -62,7 +62,7 @@ export interface FourthwallProductLike {
|
|
|
62
62
|
* Is this item sold at `locationId` at all?
|
|
63
63
|
*
|
|
64
64
|
* Exported because a location-scoped sync needs to SKIP items the merchant
|
|
65
|
-
* doesn't carry, and `squareToCatalogItem` can't do that for you
|
|
65
|
+
* doesn't carry, and `squareToCatalogItem` can't do that for you—it returns
|
|
66
66
|
* one item, and "don't store this row" isn't a `CatalogItem`. Without the guard
|
|
67
67
|
* an unstocked item mirrors as a $0 card with no variants, which looks like a
|
|
68
68
|
* pricing bug rather than a catalog decision.
|
|
@@ -79,7 +79,7 @@ export declare function squareItemSoldAt(item: SquareItemLike, locationId: strin
|
|
|
79
79
|
*
|
|
80
80
|
* `price` is the LOWEST variation price. A Square item is a family ("Bag of
|
|
81
81
|
* beans" with 12oz and 2lb variations), so a single headline number has to mean
|
|
82
|
-
* "from"
|
|
82
|
+
* "from"—taking the first variation's price instead would change with Square's
|
|
83
83
|
* ordering and quietly misprice the card.
|
|
84
84
|
*
|
|
85
85
|
* ## Scoping to one merchant
|
|
@@ -90,22 +90,22 @@ export declare function squareItemSoldAt(item: SquareItemLike, locationId: strin
|
|
|
90
90
|
*
|
|
91
91
|
* The headline number has to be scoped for the same reason the variants are.
|
|
92
92
|
* "From $8" computed over the whole catalog, on a page where the $8 size isn't
|
|
93
|
-
* stocked, advertises a price this merchant will never honour
|
|
93
|
+
* stocked, advertises a price this merchant will never honour—and because the
|
|
94
94
|
* dropped variation is usually the cheap one, the error runs in the direction a
|
|
95
95
|
* customer notices at the till.
|
|
96
96
|
*
|
|
97
97
|
* Unscoped behaviour is unchanged: no `locationId` means base prices and every
|
|
98
98
|
* variation, which is correct for a single-location account.
|
|
99
99
|
*
|
|
100
|
-
* An item sold nowhere at `locationId` yields no variants and a price of 0
|
|
101
|
-
*
|
|
100
|
+
* An item sold nowhere at `locationId` yields no variants and a price of 0—filter
|
|
101
|
+
* with {@link squareItemSoldAt} before calling rather than storing that.
|
|
102
102
|
*/
|
|
103
103
|
export declare function squareToCatalogItem(item: SquareItemLike, options?: SquareAdapterOptions): CatalogItem;
|
|
104
104
|
/**
|
|
105
105
|
* Fourthwall product → `CatalogItem`. Same "lowest variant wins" rule as Square,
|
|
106
106
|
* for the same reason.
|
|
107
107
|
*
|
|
108
|
-
* Fourthwall already prices in major units, so there's no conversion
|
|
108
|
+
* Fourthwall already prices in major units, so there's no conversion—mirroring
|
|
109
109
|
* `lowestPrice` in `louise-toolkit/commerce/fourthwall`.
|
|
110
110
|
*/
|
|
111
111
|
export declare function fourthwallToCatalogItem(product: FourthwallProductLike): CatalogItem;
|
|
@@ -4,20 +4,20 @@
|
|
|
4
4
|
//
|
|
5
5
|
// This is the file the whole module exists for. themidwestartist.com's loader
|
|
6
6
|
// says it outright: coracle runs the same helper over Square, "only the
|
|
7
|
-
// content/repo reads differ
|
|
7
|
+
// content/repo reads differ—issue: repo drift." Two sites, one intent, two
|
|
8
8
|
// hand-written translations that drifted apart. The translation is mechanical,
|
|
9
9
|
// so it belongs here once.
|
|
10
10
|
//
|
|
11
11
|
// Each provider's client already returns a normalized-for-that-provider shape
|
|
12
12
|
// (`SquareCatalogItem`, `FwProduct`); these functions take that last step to the
|
|
13
|
-
// shape the mirror stores. Deliberately pure
|
|
13
|
+
// shape the mirror stores. Deliberately pure—they take the provider's objects,
|
|
14
14
|
// not credentials or an `env`, so they're trivially testable and the caller
|
|
15
15
|
// keeps control of how the fetch happens (cached, paged, rate-limited).
|
|
16
16
|
/**
|
|
17
17
|
* Is this object sold at `locationId`?
|
|
18
18
|
*
|
|
19
19
|
* Mirrors `presentAt` in `louise-toolkit/commerce/square`, but tolerant of the
|
|
20
|
-
* fields being absent. The two lists are NOT symmetric
|
|
20
|
+
* fields being absent. The two lists are NOT symmetric—`presentAtLocationIds`
|
|
21
21
|
* is a whitelist consulted when `presentAtAllLocations` is false,
|
|
22
22
|
* `absentAtLocationIds` a blacklist consulted when it is true. Reading them the
|
|
23
23
|
* other way round shows a merchant products they do not carry.
|
|
@@ -40,7 +40,7 @@ const toMajor = (cents) => Math.round(cents) / 100;
|
|
|
40
40
|
* Is this item sold at `locationId` at all?
|
|
41
41
|
*
|
|
42
42
|
* Exported because a location-scoped sync needs to SKIP items the merchant
|
|
43
|
-
* doesn't carry, and `squareToCatalogItem` can't do that for you
|
|
43
|
+
* doesn't carry, and `squareToCatalogItem` can't do that for you—it returns
|
|
44
44
|
* one item, and "don't store this row" isn't a `CatalogItem`. Without the guard
|
|
45
45
|
* an unstocked item mirrors as a $0 card with no variants, which looks like a
|
|
46
46
|
* pricing bug rather than a catalog decision.
|
|
@@ -61,7 +61,7 @@ export function squareItemSoldAt(item, locationId) {
|
|
|
61
61
|
*
|
|
62
62
|
* `price` is the LOWEST variation price. A Square item is a family ("Bag of
|
|
63
63
|
* beans" with 12oz and 2lb variations), so a single headline number has to mean
|
|
64
|
-
* "from"
|
|
64
|
+
* "from"—taking the first variation's price instead would change with Square's
|
|
65
65
|
* ordering and quietly misprice the card.
|
|
66
66
|
*
|
|
67
67
|
* ## Scoping to one merchant
|
|
@@ -72,15 +72,15 @@ export function squareItemSoldAt(item, locationId) {
|
|
|
72
72
|
*
|
|
73
73
|
* The headline number has to be scoped for the same reason the variants are.
|
|
74
74
|
* "From $8" computed over the whole catalog, on a page where the $8 size isn't
|
|
75
|
-
* stocked, advertises a price this merchant will never honour
|
|
75
|
+
* stocked, advertises a price this merchant will never honour—and because the
|
|
76
76
|
* dropped variation is usually the cheap one, the error runs in the direction a
|
|
77
77
|
* customer notices at the till.
|
|
78
78
|
*
|
|
79
79
|
* Unscoped behaviour is unchanged: no `locationId` means base prices and every
|
|
80
80
|
* variation, which is correct for a single-location account.
|
|
81
81
|
*
|
|
82
|
-
* An item sold nowhere at `locationId` yields no variants and a price of 0
|
|
83
|
-
*
|
|
82
|
+
* An item sold nowhere at `locationId` yields no variants and a price of 0—filter
|
|
83
|
+
* with {@link squareItemSoldAt} before calling rather than storing that.
|
|
84
84
|
*/
|
|
85
85
|
export function squareToCatalogItem(item, options = {}) {
|
|
86
86
|
const locationId = options.locationId;
|
|
@@ -117,7 +117,7 @@ export function squareToCatalogItem(item, options = {}) {
|
|
|
117
117
|
* Fourthwall product → `CatalogItem`. Same "lowest variant wins" rule as Square,
|
|
118
118
|
* for the same reason.
|
|
119
119
|
*
|
|
120
|
-
* Fourthwall already prices in major units, so there's no conversion
|
|
120
|
+
* Fourthwall already prices in major units, so there's no conversion—mirroring
|
|
121
121
|
* `lowestPrice` in `louise-toolkit/commerce/fourthwall`.
|
|
122
122
|
*/
|
|
123
123
|
export function fourthwallToCatalogItem(product) {
|
|
@@ -2,7 +2,7 @@ import type { AstroidConfig } from "../config.js";
|
|
|
2
2
|
/** Does this project take card payments in-page? Square storefront only. */
|
|
3
3
|
export declare function usesCardCheckout(config: AstroidConfig): boolean;
|
|
4
4
|
/**
|
|
5
|
-
* `src/pages/api/checkout.ts
|
|
5
|
+
* `src/pages/api/checkout.ts`—the server-authoritative payment route.
|
|
6
6
|
*
|
|
7
7
|
* Scaffold-once: a real store adds shipping, tax, an order record, a receipt
|
|
8
8
|
* email. What Astroid fixes is the sequence, because every step of it is a place
|
|
@@ -12,7 +12,7 @@ export declare function usesCardCheckout(config: AstroidConfig): boolean;
|
|
|
12
12
|
*/
|
|
13
13
|
export declare function generateAstroidCheckoutRoute(config: AstroidConfig): string | null;
|
|
14
14
|
/**
|
|
15
|
-
* `src/components/SquareCard.astro
|
|
15
|
+
* `src/components/SquareCard.astro`—the card input.
|
|
16
16
|
*
|
|
17
17
|
* Square's Web Payments SDK renders the field in an iframe from their CDN and
|
|
18
18
|
* hands back a single-use token, so the raw card number never touches the Worker
|
|
@@ -29,13 +29,13 @@ export declare function generateAstroidSquareCard(config: AstroidConfig): string
|
|
|
29
29
|
* application id is shipped to the browser by design, and the environment is a
|
|
30
30
|
* choice, not a credential. Putting them in `credentials` would also fold them
|
|
31
31
|
* into the dormancy gate, which is about whether the module can safely CALL
|
|
32
|
-
* Square
|
|
32
|
+
* Square—a different question from whether the card field can render.
|
|
33
33
|
*
|
|
34
34
|
* The two vars are gated SEPARATELY, and that split is load-bearing.
|
|
35
35
|
* `SQUARE_ENVIRONMENT` selects the API HOST for every Square call, so it belongs
|
|
36
36
|
* to any project that talks to Square at all; `SQUARE_APP_ID` only mounts the
|
|
37
|
-
* browser card field. Gating both on card checkout
|
|
38
|
-
* `invoicing: "square"` became expressible
|
|
37
|
+
* browser card field. Gating both on card checkout—as this did until
|
|
38
|
+
* `invoicing: "square"` became expressible—left a site that runs Square for
|
|
39
39
|
* invoicing alone with no `SQUARE_ENVIRONMENT`, and `SquareConfig.environment`
|
|
40
40
|
* defaults to "sandbox". Every production invoice would have been created
|
|
41
41
|
* against the sandbox: no error, no warning, just money that never arrives.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
2
|
//
|
|
3
|
-
// The checkout SEAM
|
|
3
|
+
// The checkout SEAM—the piece that made `archetype: "storefront"` a storefront
|
|
4
4
|
// that couldn't take a card.
|
|
5
5
|
//
|
|
6
6
|
// Everything around this already existed: `verifyCheckout` re-prices server-side,
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// missing was the route in the middle, so none of it was reachable.
|
|
11
11
|
//
|
|
12
12
|
// SCOPE, deliberately narrow. This generates the server-authoritative PAYMENT
|
|
13
|
-
// path and a card input
|
|
13
|
+
// path and a card input—not a cart, not a checkout page, not shipping or tax.
|
|
14
14
|
// Where a cart lives (localStorage, D1, a portal session), what it holds, and how
|
|
15
15
|
// it renders are project decisions Astroid has no business making, and a
|
|
16
16
|
// half-opinionated cart is worse than none. What is NOT a project decision is the
|
|
@@ -26,7 +26,7 @@ export function usesCardCheckout(config) {
|
|
|
26
26
|
return astroidCommerceRoles(config.commerce).storefront === "square";
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* `src/pages/api/checkout.ts
|
|
29
|
+
* `src/pages/api/checkout.ts`—the server-authoritative payment route.
|
|
30
30
|
*
|
|
31
31
|
* Scaffold-once: a real store adds shipping, tax, an order record, a receipt
|
|
32
32
|
* email. What Astroid fixes is the sequence, because every step of it is a place
|
|
@@ -42,12 +42,12 @@ export function generateAstroidCheckoutRoute(config) {
|
|
|
42
42
|
return [
|
|
43
43
|
"// Server-authoritative checkout (POST /api/checkout).",
|
|
44
44
|
"//",
|
|
45
|
-
"// Scaffolded once and yours to extend
|
|
45
|
+
"// Scaffolded once and yours to extend—shipping, tax, an order row, a",
|
|
46
46
|
"// receipt email all belong here. What should NOT change is the ORDER of the",
|
|
47
47
|
"// steps below; each one is load-bearing:",
|
|
48
48
|
"//",
|
|
49
49
|
multi
|
|
50
|
-
? "// 0. Resolve WHICH MERCHANT this sale belongs to, from the host
|
|
50
|
+
? "// 0. Resolve WHICH MERCHANT this sale belongs to, from the host—never\n" +
|
|
51
51
|
"// from the request body. See `resolveLocationId` below."
|
|
52
52
|
: null,
|
|
53
53
|
multi
|
|
@@ -65,7 +65,7 @@ export function generateAstroidCheckoutRoute(config) {
|
|
|
65
65
|
"// a double-clicked Pay button charges once while two customers buying",
|
|
66
66
|
"// the same thing stay two charges.",
|
|
67
67
|
"// 4. Charge only when commerce is actually provisioned. With placeholder",
|
|
68
|
-
"// secrets this simulates instead
|
|
68
|
+
"// secrets this simulates instead—it must never call Square with a",
|
|
69
69
|
"// dummy credential.",
|
|
70
70
|
multi
|
|
71
71
|
? "//\n" +
|
|
@@ -95,13 +95,13 @@ export function generateAstroidCheckoutRoute(config) {
|
|
|
95
95
|
"// ── Which merchant is this? ────────────────────────────────────────────────",
|
|
96
96
|
"//",
|
|
97
97
|
'// `square.locations: "multi"` means the location is a property of the',
|
|
98
|
-
"// REQUEST, not of the environment
|
|
98
|
+
"// REQUEST, not of the environment—which is why Astroid does not require a",
|
|
99
99
|
"// SQUARE_LOCATION_ID for this project. Fill this in and keep two rules:",
|
|
100
100
|
"//",
|
|
101
101
|
"// * Derive it from the HOST (or an authenticated session), never from the",
|
|
102
102
|
"// request body. A body-supplied location lets a customer name the",
|
|
103
103
|
"// cheapest merchant's id and pay that price at the dearest merchant's",
|
|
104
|
-
"// shop
|
|
104
|
+
"// shop—the same exploit as a client-supplied price, one level back.",
|
|
105
105
|
"// * Return null for anything unrecognised. Falling back to a default",
|
|
106
106
|
"// rings one merchant's sale against another merchant's books, and looks",
|
|
107
107
|
"// completely successful while doing it.",
|
|
@@ -125,7 +125,7 @@ export function generateAstroidCheckoutRoute(config) {
|
|
|
125
125
|
" // mirror holds ONE price per item, so it structurally cannot answer",
|
|
126
126
|
' // "what does this cost at this location". `retrieveVariationPricesAt`',
|
|
127
127
|
" // resolves `location_overrides`, and omits any variation the merchant",
|
|
128
|
-
" // does not carry
|
|
128
|
+
" // does not carry—so an unstocked id fails closed as `unavailable`",
|
|
129
129
|
" // instead of silently selling at the base price.",
|
|
130
130
|
" const locationId = scope?.locationId;",
|
|
131
131
|
" if (!locationId) return new Map();",
|
|
@@ -150,7 +150,7 @@ export function generateAstroidCheckoutRoute(config) {
|
|
|
150
150
|
" const prices = new Map<string, number>();",
|
|
151
151
|
" for (const item of items) {",
|
|
152
152
|
" // The mirror stores MAJOR units (dollars); the charge is in minor units.",
|
|
153
|
-
" // `Math.round` is not decoration
|
|
153
|
+
" // `Math.round` is not decoration—19.99 * 100 is 1998.9999999999998, and",
|
|
154
154
|
" // a float cent here fails the exact-equality staleness check on every",
|
|
155
155
|
" // single checkout.",
|
|
156
156
|
" if (variantIds.includes(item.externalId)) {",
|
|
@@ -174,7 +174,7 @@ export function generateAstroidCheckoutRoute(config) {
|
|
|
174
174
|
" // or probe prices, even though the card token itself is single-use. Requires",
|
|
175
175
|
" // an Origin/Referer matching the host; a non-browser caller (a stripped",
|
|
176
176
|
" // header) is refused. If you deliberately serve checkout from another origin,",
|
|
177
|
-
" // this is the line to relax
|
|
177
|
+
" // this is the line to relax—it's yours.",
|
|
178
178
|
' if (!isSameOrigin(request)) return json({ error: "Forbidden" }, 403);',
|
|
179
179
|
"",
|
|
180
180
|
" const body = (await request.json().catch(() => null)) as {",
|
|
@@ -203,7 +203,7 @@ export function generateAstroidCheckoutRoute(config) {
|
|
|
203
203
|
' return json({ error: "This storefront is not open for orders." }, 409);',
|
|
204
204
|
" }",
|
|
205
205
|
"",
|
|
206
|
-
" // 4, EARLY
|
|
206
|
+
" // 4, EARLY—and the ordering is the point. Re-pricing per location is",
|
|
207
207
|
" // itself a live Square call, so the dormancy gate has to precede",
|
|
208
208
|
" // verification here rather than follow it; running it after would call",
|
|
209
209
|
" // Square with a placeholder token, which this route must never do.",
|
|
@@ -231,7 +231,7 @@ export function generateAstroidCheckoutRoute(config) {
|
|
|
231
231
|
" const check = await verifyCheckout(body.lines, serverPrices, {",
|
|
232
232
|
" scope: { locationId },",
|
|
233
233
|
" });",
|
|
234
|
-
" // Every stale line, with its live price
|
|
234
|
+
" // Every stale line, with its live price—feed `issues` to `repairCart`",
|
|
235
235
|
" // (louise-toolkit/commerce) to fix the whole cart in one step.",
|
|
236
236
|
" if (!check.ok) {",
|
|
237
237
|
" return json({ error: check.message, reason: check.reason, issues: check.issues }, 409);",
|
|
@@ -243,7 +243,7 @@ export function generateAstroidCheckoutRoute(config) {
|
|
|
243
243
|
: [
|
|
244
244
|
" // 1 + 2: re-price and refuse on mismatch.",
|
|
245
245
|
" const check = await verifyCheckout(body.lines, serverPrices);",
|
|
246
|
-
" // Every stale line, with its live price
|
|
246
|
+
" // Every stale line, with its live price—feed `issues` to `repairCart`",
|
|
247
247
|
" // (louise-toolkit/commerce) to fix the whole cart in one step.",
|
|
248
248
|
" if (!check.ok) {",
|
|
249
249
|
" return json({ error: check.message, reason: check.reason, issues: check.issues }, 409);",
|
|
@@ -253,7 +253,7 @@ export function generateAstroidCheckoutRoute(config) {
|
|
|
253
253
|
' const idempotencyKey = await checkoutIdempotencyKey(check, "order", cartId);',
|
|
254
254
|
"",
|
|
255
255
|
" // 4: dormant until provisioned. An unconfigured store still re-prices and",
|
|
256
|
-
" // still refuses a stale cart
|
|
256
|
+
" // still refuses a stale cart—it just doesn't move money.",
|
|
257
257
|
" // Cast as the toolkit's own `astroidModuleStatus` does: `readSecret`",
|
|
258
258
|
" // accepts a plain string OR a Secrets Store binding, which CloudflareEnv",
|
|
259
259
|
" // types more narrowly than the resolver's `SecretSource` map.",
|
|
@@ -278,7 +278,7 @@ export function generateAstroidCheckoutRoute(config) {
|
|
|
278
278
|
"",
|
|
279
279
|
" // Both are guaranteed real by the dormancy gate above; the narrowing here",
|
|
280
280
|
" // is for the type system, which can't know that. `environment` is a UNION",
|
|
281
|
-
' // ("sandbox" | "production"), not a free string
|
|
281
|
+
' // ("sandbox" | "production"), not a free string—an unrecognised value',
|
|
282
282
|
" // would otherwise silently select the sandbox host in production.",
|
|
283
283
|
' const environment = env.SQUARE_ENVIRONMENT === "production" ? "production" : "sandbox";',
|
|
284
284
|
" const payment = await createPayment(",
|
|
@@ -291,7 +291,7 @@ export function generateAstroidCheckoutRoute(config) {
|
|
|
291
291
|
" // The SERVER's number, never the client's.",
|
|
292
292
|
' amountMoney: { amount: check.subtotalCents, currency: "USD" },',
|
|
293
293
|
multi
|
|
294
|
-
? " // The location the cart was PRICED against
|
|
294
|
+
? " // The location the cart was PRICED against—necessarily the same one,\n" +
|
|
295
295
|
" // or the sale rings against a merchant who never quoted this total."
|
|
296
296
|
: null,
|
|
297
297
|
multi ? " locationId," : ' locationId: env.SQUARE_LOCATION_ID ?? "",',
|
|
@@ -319,7 +319,7 @@ export function generateAstroidCheckoutRoute(config) {
|
|
|
319
319
|
.join("\n");
|
|
320
320
|
}
|
|
321
321
|
/**
|
|
322
|
-
* `src/components/SquareCard.astro
|
|
322
|
+
* `src/components/SquareCard.astro`—the card input.
|
|
323
323
|
*
|
|
324
324
|
* Square's Web Payments SDK renders the field in an iframe from their CDN and
|
|
325
325
|
* hands back a single-use token, so the raw card number never touches the Worker
|
|
@@ -336,7 +336,7 @@ export function generateAstroidSquareCard(config) {
|
|
|
336
336
|
"---",
|
|
337
337
|
"// Square Web Payments card input.",
|
|
338
338
|
"//",
|
|
339
|
-
"// The card field is an IFRAME served by Square's CDN
|
|
339
|
+
"// The card field is an IFRAME served by Square's CDN—the raw number never",
|
|
340
340
|
"// enters this page's DOM and never reaches the Worker, which is what keeps the",
|
|
341
341
|
"// site out of PCI scope. `tokenize()` returns a single-use token; POST it to",
|
|
342
342
|
"// /api/checkout, which re-prices server-side before charging.",
|
|
@@ -348,7 +348,7 @@ export function generateAstroidSquareCard(config) {
|
|
|
348
348
|
...(multi
|
|
349
349
|
? [
|
|
350
350
|
"// Multi-location: the merchant is a property of the PAGE, so the id comes",
|
|
351
|
-
"// in as a prop. Only the card iframe is bound to it
|
|
351
|
+
"// in as a prop. Only the card iframe is bound to it—the charge takes its",
|
|
352
352
|
"// location from the server, which resolves it independently in",
|
|
353
353
|
"// /api/checkout. This one cannot pick the merchant, and shouldn't: it is",
|
|
354
354
|
"// rendered from markup a customer can reach.",
|
|
@@ -359,7 +359,7 @@ export function generateAstroidSquareCard(config) {
|
|
|
359
359
|
"",
|
|
360
360
|
]
|
|
361
361
|
: []),
|
|
362
|
-
"// The PUBLIC application id
|
|
362
|
+
"// The PUBLIC application id—safe in the browser, unlike the access token.",
|
|
363
363
|
"// Absent (an unprovisioned store) → render nothing rather than a dead form.",
|
|
364
364
|
"const appId = env.SQUARE_APP_ID;",
|
|
365
365
|
multi ? null : "const locationId = env.SQUARE_LOCATION_ID;",
|
|
@@ -417,7 +417,7 @@ export function generateAstroidSquareCard(config) {
|
|
|
417
417
|
.filter((line) => line !== null)
|
|
418
418
|
.join("\n");
|
|
419
419
|
}
|
|
420
|
-
/** Does this project talk to Square in ANY role
|
|
420
|
+
/** Does this project talk to Square in ANY role—storefront, invoicing, or
|
|
421
421
|
* otherwise? Distinct from {@link usesCardCheckout}, which asks the narrower
|
|
422
422
|
* question of whether the in-page card field renders. */
|
|
423
423
|
function usesSquare(config) {
|
|
@@ -430,13 +430,13 @@ function usesSquare(config) {
|
|
|
430
430
|
* application id is shipped to the browser by design, and the environment is a
|
|
431
431
|
* choice, not a credential. Putting them in `credentials` would also fold them
|
|
432
432
|
* into the dormancy gate, which is about whether the module can safely CALL
|
|
433
|
-
* Square
|
|
433
|
+
* Square—a different question from whether the card field can render.
|
|
434
434
|
*
|
|
435
435
|
* The two vars are gated SEPARATELY, and that split is load-bearing.
|
|
436
436
|
* `SQUARE_ENVIRONMENT` selects the API HOST for every Square call, so it belongs
|
|
437
437
|
* to any project that talks to Square at all; `SQUARE_APP_ID` only mounts the
|
|
438
|
-
* browser card field. Gating both on card checkout
|
|
439
|
-
* `invoicing: "square"` became expressible
|
|
438
|
+
* browser card field. Gating both on card checkout—as this did until
|
|
439
|
+
* `invoicing: "square"` became expressible—left a site that runs Square for
|
|
440
440
|
* invoicing alone with no `SQUARE_ENVIRONMENT`, and `SquareConfig.environment`
|
|
441
441
|
* defaults to "sandbox". Every production invoice would have been created
|
|
442
442
|
* against the sandbox: no error, no warning, just money that never arrives.
|
|
@@ -459,11 +459,11 @@ export function astroidCheckoutVars(config) {
|
|
|
459
459
|
* substitutes into `src/env.d.ts`. Empty without card checkout.
|
|
460
460
|
*/
|
|
461
461
|
export function generateAstroidCheckoutEnv(config) {
|
|
462
|
-
// Mirrors the gating in `astroidCheckoutVars
|
|
462
|
+
// Mirrors the gating in `astroidCheckoutVars`—the app id is card-checkout
|
|
463
463
|
// only, the environment belongs to any project that calls Square at all.
|
|
464
464
|
const lines = [];
|
|
465
465
|
if (usesCardCheckout(config)) {
|
|
466
|
-
lines.push(" /** Square's PUBLIC application id
|
|
466
|
+
lines.push(" /** Square's PUBLIC application id—shipped to the browser to mount the", " * Web Payments card field. Not a secret; see wrangler.jsonc `vars`. */", " SQUARE_APP_ID: string;");
|
|
467
467
|
}
|
|
468
468
|
if (usesSquare(config)) {
|
|
469
469
|
lines.push(' /** Square API environment: "sandbox" or "production". Selects the API', " * host for EVERY Square call, so it is required for invoicing too. */", " SQUARE_ENVIRONMENT: string;");
|
|
@@ -28,13 +28,13 @@ export interface VerifiedLine {
|
|
|
28
28
|
* worth a notify-me. Collapsing them tells someone to remove an item the shop
|
|
29
29
|
* will restock on Tuesday.
|
|
30
30
|
*
|
|
31
|
-
* A lookup can only produce `"out-of-stock"` by saying so
|
|
32
|
-
* {@link ScopedPriceLookup}
|
|
31
|
+
* A lookup can only produce `"out-of-stock"` by saying so—see
|
|
32
|
+
* {@link ScopedPriceLookup}—because a bare `Map` has no way to distinguish
|
|
33
33
|
* them and guessing would put the wrong sentence on the screen.
|
|
34
34
|
*/
|
|
35
35
|
export type CheckoutRefusal = "empty" | "unavailable" | "out-of-stock" | "price-changed" | "invalid";
|
|
36
36
|
/**
|
|
37
|
-
* One way the cart disagrees with the live catalog
|
|
37
|
+
* One way the cart disagrees with the live catalog—the toolkit's
|
|
38
38
|
* `CartIssue`, less the add-on case (checkout lines carry no add-ons yet).
|
|
39
39
|
* Hand the list to `repairCart` from `louise-toolkit/commerce` to fix the cart
|
|
40
40
|
* in one step.
|
|
@@ -52,7 +52,7 @@ export type CheckoutVerification = {
|
|
|
52
52
|
reason: CheckoutRefusal;
|
|
53
53
|
message: string;
|
|
54
54
|
/**
|
|
55
|
-
* Every problem, in cart order, with what the catalog says now
|
|
55
|
+
* Every problem, in cart order, with what the catalog says now—empty
|
|
56
56
|
* for `"empty"` and `"invalid"`, which are about the request, not the
|
|
57
57
|
* catalog.
|
|
58
58
|
*/
|
|
@@ -62,7 +62,7 @@ export type CheckoutVerification = {
|
|
|
62
62
|
* map omits is treated as no longer purchasable. */
|
|
63
63
|
export type PriceLookup = (variantIds: string[]) => Promise<Map<string, number>>;
|
|
64
64
|
/** Where the sale is happening. Optional, and providers without a location
|
|
65
|
-
* dimension ignore it
|
|
65
|
+
* dimension ignore it—a single-merchant Square account or Fourthwall store
|
|
66
66
|
* passes nothing and behaves exactly as before. */
|
|
67
67
|
export interface CheckoutScope {
|
|
68
68
|
/** Provider location id (Square) or equivalent merchant key. */
|
|
@@ -86,13 +86,13 @@ export interface ScopedPrices {
|
|
|
86
86
|
* A price lookup that knows WHERE the sale is happening.
|
|
87
87
|
*
|
|
88
88
|
* This is the multi-merchant checkout guard. One shared catalog sold through
|
|
89
|
-
* several merchants carries a different price per location
|
|
90
|
-
* commission is absorbed in its own override
|
|
89
|
+
* several merchants carries a different price per location—each shop's
|
|
90
|
+
* commission is absorbed in its own override—so re-pricing a cart against
|
|
91
91
|
* base prices lets a customer pay the cheapest merchant's price at the dearest
|
|
92
92
|
* merchant's storefront. That is not a rounding error; it is the same class of
|
|
93
93
|
* bug as trusting the client's `unitPriceCents`, just one level further back.
|
|
94
94
|
*
|
|
95
|
-
* `scope` is optional so a {@link PriceLookup} is still assignable here
|
|
95
|
+
* `scope` is optional so a {@link PriceLookup} is still assignable here—an
|
|
96
96
|
* existing single-location lookup simply ignores the extra argument, which is
|
|
97
97
|
* exactly what a function of lower arity does in JavaScript.
|
|
98
98
|
*
|
|
@@ -131,7 +131,7 @@ export declare function verifyCheckout(lines: unknown, lookup: ScopedPriceLookup
|
|
|
131
131
|
/**
|
|
132
132
|
* A deterministic idempotency key for one buyer's checkout attempt.
|
|
133
133
|
*
|
|
134
|
-
* Providers dedupe on this, so the same key must mean the same charge
|
|
134
|
+
* Providers dedupe on this, so the same key must mean the same charge—which
|
|
135
135
|
* cuts both ways, and the second direction is the one that costs money. It is
|
|
136
136
|
* derived from the verified cart *and* `identity`, not from a random value or a
|
|
137
137
|
* timestamp: a customer double-clicking Pay sends the same key twice and is
|
|
@@ -141,18 +141,18 @@ export declare function verifyCheckout(lines: unknown, lookup: ScopedPriceLookup
|
|
|
141
141
|
* **`identity` is required, and it is what makes the key safe.** Without it the
|
|
142
142
|
* key was a pure function of the cart contents, so two DIFFERENT customers
|
|
143
143
|
* buying the same thing for the same price produced byte-identical keys. Stripe
|
|
144
|
-
* and Square scope idempotency keys per account and retain them for
|
|
144
|
+
* and Square scope idempotency keys per account and retain them for about 24 hours, so the
|
|
145
145
|
* provider replayed the first customer's PaymentIntent instead of creating the
|
|
146
146
|
* second's: the second buyer was never charged, no second order existed, and the
|
|
147
147
|
* site reported success. On a single-SKU storefront that is ordinary traffic,
|
|
148
148
|
* not an edge case.
|
|
149
149
|
*
|
|
150
150
|
* Pass something stable across a retry of THIS attempt and distinct between
|
|
151
|
-
* buyers
|
|
151
|
+
* buyers—a cart id, a checkout-session id, or a portal user id. Do not pass a
|
|
152
152
|
* value that varies per request (a fresh uuid defeats the dedupe and a
|
|
153
153
|
* double-click charges twice), and do not pass a constant.
|
|
154
154
|
*
|
|
155
|
-
* `scope` remains the OPERATION
|
|
155
|
+
* `scope` remains the OPERATION—`"order"` vs `"refund"`—so the two can never
|
|
156
156
|
* collide for one buyer. It is not an identity and never was.
|
|
157
157
|
*/
|
|
158
158
|
export declare function checkoutIdempotencyKey(verified: {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Server-authoritative checkout.
|
|
4
4
|
//
|
|
5
5
|
// A cart arrives from the browser, so every number in it is a claim, not a fact.
|
|
6
|
-
// The rule this encodes
|
|
6
|
+
// The rule this encodes—taken from coracle.coffee's working checkout—is that
|
|
7
7
|
// the client's price is a **staleness check**, never an input to the charge:
|
|
8
8
|
// look the price up server-side, and if it disagrees with what the customer was
|
|
9
9
|
// shown, refuse rather than silently charging a different amount. Refusing is
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// The comparison itself is the toolkit's `cartIssues`, which reports EVERY
|
|
17
17
|
// stale line rather than the first: a refusal that names one problem at a time
|
|
18
18
|
// is how a customer ends up fixing a line, retrying, and being refused over the
|
|
19
|
-
// next. What this adds is the opinion
|
|
19
|
+
// next. What this adds is the opinion—policing the untrusted body, and the
|
|
20
20
|
// sentence a customer sees.
|
|
21
21
|
import { cartIssues } from "louise-toolkit/commerce";
|
|
22
22
|
import { AstroidUsageError } from "../errors.js";
|
|
@@ -91,7 +91,7 @@ lookup, options = {}) {
|
|
|
91
91
|
}
|
|
92
92
|
const { prices, outOfStock } = normalizeLookup(await lookup([...new Set(parsed.map((l) => l.variantId))], options.scope));
|
|
93
93
|
// No `liveModifierIds`, so the add-on check is skipped and every issue is a
|
|
94
|
-
// variant one
|
|
94
|
+
// variant one—which is what makes the narrowing to CheckoutIssue true.
|
|
95
95
|
const issues = cartIssues(parsed, { prices, outOfStock });
|
|
96
96
|
const [first] = issues;
|
|
97
97
|
if (first)
|
|
@@ -109,7 +109,7 @@ lookup, options = {}) {
|
|
|
109
109
|
/**
|
|
110
110
|
* A deterministic idempotency key for one buyer's checkout attempt.
|
|
111
111
|
*
|
|
112
|
-
* Providers dedupe on this, so the same key must mean the same charge
|
|
112
|
+
* Providers dedupe on this, so the same key must mean the same charge—which
|
|
113
113
|
* cuts both ways, and the second direction is the one that costs money. It is
|
|
114
114
|
* derived from the verified cart *and* `identity`, not from a random value or a
|
|
115
115
|
* timestamp: a customer double-clicking Pay sends the same key twice and is
|
|
@@ -119,18 +119,18 @@ lookup, options = {}) {
|
|
|
119
119
|
* **`identity` is required, and it is what makes the key safe.** Without it the
|
|
120
120
|
* key was a pure function of the cart contents, so two DIFFERENT customers
|
|
121
121
|
* buying the same thing for the same price produced byte-identical keys. Stripe
|
|
122
|
-
* and Square scope idempotency keys per account and retain them for
|
|
122
|
+
* and Square scope idempotency keys per account and retain them for about 24 hours, so the
|
|
123
123
|
* provider replayed the first customer's PaymentIntent instead of creating the
|
|
124
124
|
* second's: the second buyer was never charged, no second order existed, and the
|
|
125
125
|
* site reported success. On a single-SKU storefront that is ordinary traffic,
|
|
126
126
|
* not an edge case.
|
|
127
127
|
*
|
|
128
128
|
* Pass something stable across a retry of THIS attempt and distinct between
|
|
129
|
-
* buyers
|
|
129
|
+
* buyers—a cart id, a checkout-session id, or a portal user id. Do not pass a
|
|
130
130
|
* value that varies per request (a fresh uuid defeats the dedupe and a
|
|
131
131
|
* double-click charges twice), and do not pass a constant.
|
|
132
132
|
*
|
|
133
|
-
* `scope` remains the OPERATION
|
|
133
|
+
* `scope` remains the OPERATION—`"order"` vs `"refund"`—so the two can never
|
|
134
134
|
* collide for one buyer. It is not an identity and never was.
|
|
135
135
|
*/
|
|
136
136
|
export async function checkoutIdempotencyKey(verified, scope, identity) {
|