@saastemly/voidcommerce 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +271 -0
- package/bin/vc +2 -0
- package/dist/catalog.d.ts +69 -0
- package/dist/catalog.js +34 -0
- package/dist/cli.d.ts +24 -0
- package/dist/cli.js +544 -0
- package/dist/deploy/cloudflare.d.ts +25 -0
- package/dist/deploy/index.d.ts +16 -0
- package/dist/deploy/jsonc.d.ts +8 -0
- package/dist/deploy/preflight.d.ts +29 -0
- package/dist/deploy/wrangler.d.ts +37 -0
- package/dist/dist.d.ts +22 -0
- package/dist/generate/auth.d.ts +2 -0
- package/dist/generate/ci.d.ts +24 -0
- package/dist/generate/env.d.ts +13 -0
- package/dist/generate/frontend.d.ts +47 -0
- package/dist/generate/index.d.ts +28 -0
- package/dist/generate/requirements.d.ts +13 -0
- package/dist/generate/strict.d.ts +72 -0
- package/dist/generate/support.d.ts +23 -0
- package/dist/help.d.ts +31 -0
- package/dist/import.d.ts +2 -0
- package/dist/index-s7sq41qs.js +590 -0
- package/dist/index-ssv3a6wc.js +172 -0
- package/dist/index-wzy1xtr1.js +3155 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +190 -0
- package/dist/init.d.ts +1 -0
- package/dist/manifest.d.ts +131 -0
- package/dist/manifest.js +41 -0
- package/dist/project.d.ts +20 -0
- package/dist/regenerate.d.ts +9 -0
- package/dist/scripts.d.ts +12 -0
- package/dist/void.d.ts +30 -0
- package/dist/wizard.d.ts +7 -0
- package/package.json +50 -0
- package/src/catalog.ts +673 -0
- package/src/cli.ts +78 -0
- package/src/deploy/cloudflare.ts +166 -0
- package/src/deploy/index.ts +101 -0
- package/src/deploy/jsonc.ts +148 -0
- package/src/deploy/preflight.ts +137 -0
- package/src/deploy/wrangler.ts +111 -0
- package/src/dist.ts +157 -0
- package/src/generate/auth.ts +386 -0
- package/src/generate/ci.ts +208 -0
- package/src/generate/env.ts +164 -0
- package/src/generate/frontend.ts +275 -0
- package/src/generate/index.ts +390 -0
- package/src/generate/requirements.ts +48 -0
- package/src/generate/strict.ts +692 -0
- package/src/generate/support.ts +252 -0
- package/src/help.ts +172 -0
- package/src/import.ts +237 -0
- package/src/index.ts +37 -0
- package/src/init.ts +187 -0
- package/src/manifest.ts +303 -0
- package/src/project.ts +63 -0
- package/src/regenerate.ts +51 -0
- package/src/scripts.ts +53 -0
- package/src/void.ts +115 -0
- package/src/wizard.ts +234 -0
package/src/catalog.ts
ADDED
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything `vc init` can offer, and what choosing it means.
|
|
3
|
+
*
|
|
4
|
+
* ── Why a registry ───────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* The wizard is only as good as its descriptions. A checkbox that says
|
|
7
|
+
* "organization" tells nobody anything; one that says "teams that share a
|
|
8
|
+
* shop — a club ordering trophies as one account" is a decision somebody can
|
|
9
|
+
* make. So every entry carries a one-line `hint` written for the person
|
|
10
|
+
* choosing, not for the person who wrote the plugin.
|
|
11
|
+
*
|
|
12
|
+
* Each entry also carries what choosing it COSTS: the packages to install,
|
|
13
|
+
* the env keys it needs, and whether those keys are secrets. That is how the
|
|
14
|
+
* generator knows what to write into `env.ts`, `.env.example` and the
|
|
15
|
+
* deploy requirements without a second hand-kept list.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface EnvKey {
|
|
19
|
+
key: string;
|
|
20
|
+
/** What breaks without it — the sentence an operator reads at preflight. */
|
|
21
|
+
breaks: string;
|
|
22
|
+
/** Safe to commit in `.env.production`. Everything else is a secret. */
|
|
23
|
+
plaintext?: boolean | undefined;
|
|
24
|
+
/** A working value for local development. */
|
|
25
|
+
dev?: string | undefined;
|
|
26
|
+
/** Where the real value comes from. */
|
|
27
|
+
where?: string | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Choice {
|
|
31
|
+
id: string;
|
|
32
|
+
label: string;
|
|
33
|
+
hint: string;
|
|
34
|
+
/** npm packages the choice pulls in. */
|
|
35
|
+
packages?: string[] | undefined;
|
|
36
|
+
env?: EnvKey[] | undefined;
|
|
37
|
+
/** Chosen unless the person unticks it. */
|
|
38
|
+
recommended?: boolean | undefined;
|
|
39
|
+
/** Cannot be unticked; shown so the person knows it is there. */
|
|
40
|
+
required?: boolean | undefined;
|
|
41
|
+
/** Ids this choice needs; the wizard adds them. */
|
|
42
|
+
requires?: string[] | undefined;
|
|
43
|
+
/** Ids this choice cannot coexist with. */
|
|
44
|
+
conflicts?: string[] | undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface Group {
|
|
48
|
+
id: string;
|
|
49
|
+
title: string;
|
|
50
|
+
/** One line under the title. */
|
|
51
|
+
intro?: string | undefined;
|
|
52
|
+
choices: Choice[];
|
|
53
|
+
/** `select` picks exactly one; `multiselect` any number. */
|
|
54
|
+
kind: "select" | "multiselect";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* -------------------------------------------------------------------------- */
|
|
58
|
+
/* Better Auth */
|
|
59
|
+
/* -------------------------------------------------------------------------- */
|
|
60
|
+
|
|
61
|
+
export const SIGN_IN: Group = {
|
|
62
|
+
id: "signIn",
|
|
63
|
+
title: "How do people sign in?",
|
|
64
|
+
intro: "Every method proves the address; a shop needs that for the order confirmation anyway.",
|
|
65
|
+
kind: "multiselect",
|
|
66
|
+
choices: [
|
|
67
|
+
{
|
|
68
|
+
id: "magic-link",
|
|
69
|
+
label: "Magic link",
|
|
70
|
+
hint: "one tap on the device the email is open on — signs up too",
|
|
71
|
+
recommended: true,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "email-otp",
|
|
75
|
+
label: "Email code",
|
|
76
|
+
hint: "for when the email is on a different device from the checkout — signs up too",
|
|
77
|
+
recommended: true,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "passkey",
|
|
81
|
+
label: "Passkeys",
|
|
82
|
+
hint: "returning customers, no email round-trip; sign-in only",
|
|
83
|
+
packages: ["@better-auth/passkey"],
|
|
84
|
+
recommended: true,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "google",
|
|
88
|
+
label: "Google",
|
|
89
|
+
hint: "Better Auth's own social provider — not a hand-rolled OAuth flow",
|
|
90
|
+
recommended: true,
|
|
91
|
+
env: [
|
|
92
|
+
{ key: "GOOGLE_CLIENT_ID", breaks: "the Google button is drawn and fails", where: "Google Cloud console" },
|
|
93
|
+
{ key: "GOOGLE_CLIENT_SECRET", breaks: "the same", where: "Google Cloud console" },
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "apple",
|
|
98
|
+
label: "Apple",
|
|
99
|
+
hint: "required by Apple if you also offer Google in an iOS app",
|
|
100
|
+
env: [
|
|
101
|
+
{ key: "APPLE_CLIENT_ID", breaks: "the Apple button is drawn and fails", where: "Apple developer console" },
|
|
102
|
+
{ key: "APPLE_CLIENT_SECRET", breaks: "the same", where: "Apple developer console" },
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "github",
|
|
107
|
+
label: "GitHub",
|
|
108
|
+
hint: "developer-audience shops",
|
|
109
|
+
env: [
|
|
110
|
+
{ key: "GITHUB_CLIENT_ID", breaks: "the GitHub button is drawn and fails" },
|
|
111
|
+
{ key: "GITHUB_CLIENT_SECRET", breaks: "the same" },
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: "password",
|
|
116
|
+
label: "Password",
|
|
117
|
+
hint: "reused from somewhere already breached, needs a reset flow that is itself a takeover path — off unless you have a reason",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "username",
|
|
121
|
+
label: "Username",
|
|
122
|
+
hint: "sign in by a handle instead of an address; only with password",
|
|
123
|
+
requires: ["password"],
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "phone",
|
|
127
|
+
label: "Phone number",
|
|
128
|
+
hint: "SMS codes — needs an SMS provider you wire yourself",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: "anonymous",
|
|
132
|
+
label: "Guest accounts",
|
|
133
|
+
hint: "a cart before an address; upgraded to a real account on first sign-in",
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const AUTH_PLUGINS: Group = {
|
|
139
|
+
id: "auth",
|
|
140
|
+
title: "Account features",
|
|
141
|
+
intro: "Better Auth's official plugins. Each adds tables and endpoints; the admin panel grows screens for them.",
|
|
142
|
+
kind: "multiselect",
|
|
143
|
+
choices: [
|
|
144
|
+
{
|
|
145
|
+
id: "admin",
|
|
146
|
+
label: "Admin roles",
|
|
147
|
+
hint: "who may open the panel — required, because a shop has operators",
|
|
148
|
+
required: true,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: "system-user",
|
|
152
|
+
label: "System identity",
|
|
153
|
+
hint: "the deployment authenticates as ITSELF to import its own catalogue — no human sign-in to bootstrap",
|
|
154
|
+
packages: ["@saastemly/better-system-user"],
|
|
155
|
+
required: true,
|
|
156
|
+
env: [
|
|
157
|
+
{
|
|
158
|
+
key: "SYSTEM_API_KEY",
|
|
159
|
+
breaks: "nothing can be imported — no catalogue, no content, no DNS",
|
|
160
|
+
where: "openssl rand -base64 48",
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: "api-key",
|
|
166
|
+
label: "API keys",
|
|
167
|
+
hint: "machine access for integrations and the storefront build — 10 requests/day by default, raised for you",
|
|
168
|
+
packages: ["@better-auth/api-key"],
|
|
169
|
+
recommended: true,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: "two-factor",
|
|
173
|
+
label: "Two-factor",
|
|
174
|
+
hint: "TOTP for operators; a customer account rarely warrants it",
|
|
175
|
+
recommended: true,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: "organization",
|
|
179
|
+
label: "Organizations",
|
|
180
|
+
hint: "teams that share a shop account — a club ordering trophies as one buyer with several members",
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: "multi-session",
|
|
184
|
+
label: "Multiple sessions",
|
|
185
|
+
hint: "switch between accounts without signing out — support staff who impersonate",
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: "jwt",
|
|
189
|
+
label: "JWT",
|
|
190
|
+
hint: "tokens for a service that cannot hold a cookie — a mobile app, a partner API",
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: "bearer",
|
|
194
|
+
label: "Bearer tokens",
|
|
195
|
+
hint: "the session token in a header instead of a cookie; the mobile-app case",
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: "haveibeenpwned",
|
|
199
|
+
label: "Breached-password check",
|
|
200
|
+
hint: "refuses a password that has appeared in a breach; only with password sign-in",
|
|
201
|
+
requires: ["password"],
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: "one-time-token",
|
|
205
|
+
label: "One-time tokens",
|
|
206
|
+
hint: "hand a signed-in session to another origin for one request — the storefront to the panel",
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: "generic-oauth",
|
|
210
|
+
label: "Custom OAuth provider",
|
|
211
|
+
hint: "an identity provider Better Auth does not ship — a national eID, a corporate SSO",
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: "oauth-proxy",
|
|
215
|
+
label: "OAuth proxy",
|
|
216
|
+
hint: "OAuth callbacks that work on preview deployments with changing URLs",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: "siwe",
|
|
220
|
+
label: "Sign in with Ethereum",
|
|
221
|
+
hint: "wallet-based sign-in; almost certainly not for a trophy shop",
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: "device-authorization",
|
|
225
|
+
label: "Device flow",
|
|
226
|
+
hint: "sign in on a TV or a CLI by confirming on a phone",
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: "custom-session",
|
|
230
|
+
label: "Custom session data",
|
|
231
|
+
hint: "attach computed fields to every session response",
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/* -------------------------------------------------------------------------- */
|
|
237
|
+
/* betterCommerce */
|
|
238
|
+
/* -------------------------------------------------------------------------- */
|
|
239
|
+
|
|
240
|
+
export const COMMERCE_CATALOG: Group = {
|
|
241
|
+
id: "catalog",
|
|
242
|
+
title: "Catalogue",
|
|
243
|
+
kind: "multiselect",
|
|
244
|
+
choices: [
|
|
245
|
+
{
|
|
246
|
+
id: "catalog",
|
|
247
|
+
label: "Products as rows",
|
|
248
|
+
hint: "products, variants, prices, images, categories, collections, tags — editable in the panel",
|
|
249
|
+
required: true,
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
id: "catalog-import",
|
|
253
|
+
label: "Catalogue import",
|
|
254
|
+
hint: "push products from a file, a PIM or a spreadsheet — matched on handle, never deletes",
|
|
255
|
+
required: true,
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
id: "metafields",
|
|
259
|
+
label: "Custom fields",
|
|
260
|
+
hint: "typed data on any row without a new table — Shopify's metafields",
|
|
261
|
+
recommended: true,
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
id: "personalization",
|
|
265
|
+
label: "Personalization",
|
|
266
|
+
hint: "engraving, monograms, gift messages: validated at add-to-cart and priced per character, per unit or once per order",
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
id: "inventory",
|
|
270
|
+
label: "Inventory",
|
|
271
|
+
hint: "stock levels and reservations per location",
|
|
272
|
+
recommended: true,
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
id: "bundles",
|
|
276
|
+
label: "Bundles",
|
|
277
|
+
hint: "a kit is its components at a set price",
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
id: "price-lists",
|
|
281
|
+
label: "Price lists",
|
|
282
|
+
hint: "override or discount prices per customer group or date range",
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
id: "translations",
|
|
286
|
+
label: "Translations",
|
|
287
|
+
hint: "product copy in more than one language",
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: "search",
|
|
291
|
+
label: "Search",
|
|
292
|
+
hint: "a search index over the catalogue; memory locally, Meilisearch in production",
|
|
293
|
+
recommended: true,
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
export const COMMERCE_SALES: Group = {
|
|
299
|
+
id: "sales",
|
|
300
|
+
title: "Selling",
|
|
301
|
+
kind: "multiselect",
|
|
302
|
+
choices: [
|
|
303
|
+
{
|
|
304
|
+
id: "regions",
|
|
305
|
+
label: "Regions",
|
|
306
|
+
hint: "which countries you sell to, in which currency — required",
|
|
307
|
+
required: true,
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
id: "sales-channels",
|
|
311
|
+
label: "Sales channels",
|
|
312
|
+
hint: "the web shop, a POS, a marketplace — scope products and prices per channel",
|
|
313
|
+
recommended: true,
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
id: "discounts",
|
|
317
|
+
label: "Discounts and campaigns",
|
|
318
|
+
hint: "codes, automatic promotions, funded campaigns with a spend cap",
|
|
319
|
+
recommended: true,
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
id: "gift-cards",
|
|
323
|
+
label: "Gift cards",
|
|
324
|
+
hint: "sold as products, redeemed at checkout; balance derived from the ledger",
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
id: "loyalty",
|
|
328
|
+
label: "Loyalty points",
|
|
329
|
+
hint: "earn on paid orders, spend at checkout",
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
id: "subscriptions",
|
|
333
|
+
label: "Subscriptions",
|
|
334
|
+
hint: "recurring orders on a schedule",
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
id: "draft-orders",
|
|
338
|
+
label: "Draft orders",
|
|
339
|
+
hint: "an operator builds a cart for a buyer and converts it — quotes, phone orders",
|
|
340
|
+
recommended: true,
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
id: "order-edits",
|
|
344
|
+
label: "Order edits",
|
|
345
|
+
hint: "change a paid order — add a line, swap a size — with the difference charged or refunded",
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
id: "returns",
|
|
349
|
+
label: "Returns",
|
|
350
|
+
hint: "the buyer-facing road to a refund",
|
|
351
|
+
recommended: true,
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
id: "wishlists",
|
|
355
|
+
label: "Wishlists",
|
|
356
|
+
hint: "save for later; prices resolved on read, so a saved item shows today's price",
|
|
357
|
+
recommended: true,
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
id: "reviews",
|
|
361
|
+
label: "Reviews",
|
|
362
|
+
hint: "held for moderation; the verified badge is derived from a paid order, never claimed",
|
|
363
|
+
recommended: true,
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
id: "customer-groups",
|
|
367
|
+
label: "Customer groups",
|
|
368
|
+
hint: "segments with their own prices and rules — trade, VIP, staff",
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
id: "acp",
|
|
372
|
+
label: "Agentic commerce",
|
|
373
|
+
hint: "let an AI agent shop on a buyer's behalf through the Agentic Commerce Protocol",
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
export const COMMERCE_OPERATIONS: Group = {
|
|
379
|
+
id: "operations",
|
|
380
|
+
title: "Operations",
|
|
381
|
+
kind: "multiselect",
|
|
382
|
+
choices: [
|
|
383
|
+
{
|
|
384
|
+
id: "fulfillments",
|
|
385
|
+
label: "Fulfilment",
|
|
386
|
+
hint: "what happens when the money is confirmed — ship it, grant it, hand it to the ERP",
|
|
387
|
+
required: true,
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
id: "shipping-zones",
|
|
391
|
+
label: "Shipping zones",
|
|
392
|
+
hint: "what a buyer can be charged for delivery, by country — required to quote shipping",
|
|
393
|
+
required: true,
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
id: "tax-regions",
|
|
397
|
+
label: "Tax",
|
|
398
|
+
hint: "VAT by country, declared in code so a fresh deploy is never tax-free",
|
|
399
|
+
required: true,
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
id: "notifications",
|
|
403
|
+
label: "Notifications",
|
|
404
|
+
hint: "order confirmations and shipping updates, queued and retried",
|
|
405
|
+
required: true,
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
id: "nft",
|
|
409
|
+
label: "Token instead of a parcel",
|
|
410
|
+
hint: "a buyer who would rather not have the object can take an NFT of it instead — same price, same order, nothing shipped. Needs a mint provider before it mints anything",
|
|
411
|
+
recommended: false,
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
id: "events",
|
|
415
|
+
label: "Webhooks",
|
|
416
|
+
hint: "deliver commerce events to subscribers with retries and dead-lettering",
|
|
417
|
+
recommended: true,
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
id: "workflows",
|
|
421
|
+
label: "Workflows",
|
|
422
|
+
hint: "multi-step operations that survive a crash mid-way",
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
id: "files",
|
|
426
|
+
label: "Files",
|
|
427
|
+
hint: "uploads — product images, a customer's logo for engraving",
|
|
428
|
+
recommended: true,
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
id: "merchants",
|
|
432
|
+
label: "Marketplace",
|
|
433
|
+
hint: "several sellers in one shop, with payouts",
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
id: "store",
|
|
437
|
+
label: "Store settings",
|
|
438
|
+
hint: "name, support address, public settings the storefront reads",
|
|
439
|
+
required: true,
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
id: "runtime",
|
|
443
|
+
label: "Cache and locking",
|
|
444
|
+
hint: "required — the kernel's cache and lock providers",
|
|
445
|
+
required: true,
|
|
446
|
+
},
|
|
447
|
+
],
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
/* The NFT delivery choice lives with the other operations plugins: it is how
|
|
451
|
+
an order leaves the shop, not a thing in the catalogue. */
|
|
452
|
+
|
|
453
|
+
export const PAYMENT: Group = {
|
|
454
|
+
id: "payment",
|
|
455
|
+
title: "Payment rail",
|
|
456
|
+
intro: "Exactly one. Carrying two means every reader works out which half is live.",
|
|
457
|
+
kind: "select",
|
|
458
|
+
choices: [
|
|
459
|
+
{
|
|
460
|
+
id: "adyen",
|
|
461
|
+
label: "Adyen",
|
|
462
|
+
hint: "European rails, MobilePay, local methods",
|
|
463
|
+
env: [
|
|
464
|
+
{ key: "ADYEN_API_KEY", breaks: "the shop cannot take money", where: "Adyen Customer Area" },
|
|
465
|
+
{ key: "ADYEN_MERCHANT_ACCOUNT", breaks: "the same", where: "Adyen Customer Area" },
|
|
466
|
+
{ key: "ADYEN_HMAC_KEY", breaks: "webhooks cannot be verified, so no order becomes paid", where: "Adyen Customer Area" },
|
|
467
|
+
{ key: "ADYEN_ENVIRONMENT", breaks: "anything but live charges real customers on the test rail", plaintext: true, dev: "test" },
|
|
468
|
+
{ key: "ADYEN_LIVE_PREFIX", breaks: "live Checkout calls are rejected", where: "Adyen Customer Area" },
|
|
469
|
+
],
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
id: "stripe",
|
|
473
|
+
label: "Stripe",
|
|
474
|
+
hint: "the default almost everywhere else",
|
|
475
|
+
env: [
|
|
476
|
+
{ key: "STRIPE_SECRET_KEY", breaks: "the shop cannot take money", where: "Stripe dashboard" },
|
|
477
|
+
],
|
|
478
|
+
},
|
|
479
|
+
],
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
export const TAX: Group = {
|
|
483
|
+
id: "tax",
|
|
484
|
+
title: "Tax calculation",
|
|
485
|
+
kind: "select",
|
|
486
|
+
choices: [
|
|
487
|
+
{
|
|
488
|
+
id: "stripe-tax",
|
|
489
|
+
label: "Stripe Tax",
|
|
490
|
+
hint: "asked per basket, so an unregistered shop charges nothing and a registered one charges the buyer's own rate — no rate to declare and none to keep up to date",
|
|
491
|
+
recommended: true,
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
id: "declared",
|
|
495
|
+
label: "Declared rates",
|
|
496
|
+
hint: "VAT per country in code, editable in the panel — right for one or a few countries",
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
id: "taxjar",
|
|
500
|
+
label: "TaxJar",
|
|
501
|
+
hint: "US sales tax by jurisdiction",
|
|
502
|
+
env: [{ key: "TAXJAR_API_KEY", breaks: "no tax is calculated" }],
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
id: "avalara",
|
|
506
|
+
label: "Avalara",
|
|
507
|
+
hint: "enterprise tax across many jurisdictions",
|
|
508
|
+
env: [
|
|
509
|
+
{ key: "AVALARA_ACCOUNT_ID", breaks: "no tax is calculated" },
|
|
510
|
+
{ key: "AVALARA_LICENSE_KEY", breaks: "the same" },
|
|
511
|
+
],
|
|
512
|
+
},
|
|
513
|
+
],
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
export const CARRIERS: Group = {
|
|
517
|
+
id: "carriers",
|
|
518
|
+
title: "Carriers for calculated shipping rates",
|
|
519
|
+
intro: "Optional. Flat-rate shipping needs none of these.",
|
|
520
|
+
kind: "multiselect",
|
|
521
|
+
choices: [
|
|
522
|
+
{ id: "shippo", label: "Shippo", hint: "many carriers through one API", env: [{ key: "SHIPPO_API_KEY", breaks: "calculated rates fall back to flat" }] },
|
|
523
|
+
{ id: "easypost", label: "EasyPost", hint: "the same, US-leaning", env: [{ key: "EASYPOST_API_KEY", breaks: "calculated rates fall back to flat" }] },
|
|
524
|
+
{ id: "ups", label: "UPS", hint: "your own UPS account, for negotiated rates an aggregator cannot see", env: [{ key: "UPS_CLIENT_ID", breaks: "UPS rates are not quoted" }, { key: "UPS_CLIENT_SECRET", breaks: "the same" }] },
|
|
525
|
+
{ id: "fedex", label: "FedEx", hint: "your own FedEx account, for negotiated rates an aggregator cannot see", env: [{ key: "FEDEX_CLIENT_ID", breaks: "FedEx rates are not quoted" }, { key: "FEDEX_CLIENT_SECRET", breaks: "the same" }] },
|
|
526
|
+
{ id: "dhl-express", label: "DHL Express", hint: "international", env: [{ key: "DHL_API_KEY", breaks: "DHL rates are not quoted" }] },
|
|
527
|
+
{ id: "usps", label: "USPS", hint: "US domestic post — the cheapest way to ship a small parcel inside the US", env: [{ key: "USPS_CLIENT_ID", breaks: "USPS rates are not quoted" }, { key: "USPS_CLIENT_SECRET", breaks: "the same" }] },
|
|
528
|
+
{ id: "royal-mail", label: "Royal Mail", hint: "UK post, for a shop whose customers are mostly in Britain", env: [{ key: "ROYAL_MAIL_CLIENT_ID", breaks: "Royal Mail rates are not quoted" }] },
|
|
529
|
+
{ id: "aramex", label: "Aramex", hint: "the carrier to reach the Gulf and wider Middle East reliably", env: [{ key: "ARAMEX_API_KEY", breaks: "Aramex rates are not quoted" }] },
|
|
530
|
+
{ id: "doordash-drive", label: "DoorDash Drive", hint: "same-day local delivery", env: [{ key: "DOORDASH_DEVELOPER_ID", breaks: "DoorDash is not offered" }] },
|
|
531
|
+
{ id: "stuart", label: "Stuart", hint: "same-day courier, Europe", env: [{ key: "STUART_CLIENT_ID", breaks: "Stuart is not offered" }] },
|
|
532
|
+
],
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
export const ERP: Group = {
|
|
536
|
+
id: "erp",
|
|
537
|
+
title: "ERP",
|
|
538
|
+
intro: "Where a paid order goes after the shop.",
|
|
539
|
+
kind: "select",
|
|
540
|
+
choices: [
|
|
541
|
+
{ id: "none", label: "None", hint: "orders live in the shop", recommended: true },
|
|
542
|
+
{
|
|
543
|
+
id: "business-central",
|
|
544
|
+
label: "Microsoft Dynamics 365 Business Central",
|
|
545
|
+
hint: "a paid order becomes a sales order",
|
|
546
|
+
env: [
|
|
547
|
+
{ key: "BC_TENANT_ID", breaks: "paid orders never reach the ERP — the shop and the warehouse disagree", where: "Azure app registration" },
|
|
548
|
+
{ key: "BC_CLIENT_ID", breaks: "the same", where: "Azure app registration" },
|
|
549
|
+
{ key: "BC_CLIENT_SECRET", breaks: "the same", where: "Azure app registration" },
|
|
550
|
+
{ key: "BC_COMPANY_ID", breaks: "the same", where: "Business Central" },
|
|
551
|
+
{ key: "BC_ENVIRONMENT", breaks: "calls go to the wrong tenant environment", plaintext: true, dev: "production" },
|
|
552
|
+
],
|
|
553
|
+
},
|
|
554
|
+
],
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
/* -------------------------------------------------------------------------- */
|
|
558
|
+
/* Platform */
|
|
559
|
+
/* -------------------------------------------------------------------------- */
|
|
560
|
+
|
|
561
|
+
export const EMAIL: Group = {
|
|
562
|
+
id: "email",
|
|
563
|
+
title: "Email",
|
|
564
|
+
intro: "One transport for sign-in codes, magic links and order confirmations. Without it nobody can sign in, because every sign-in method proves an address.",
|
|
565
|
+
kind: "select",
|
|
566
|
+
choices: [
|
|
567
|
+
{
|
|
568
|
+
id: "cloudflare-email",
|
|
569
|
+
label: "Cloudflare Email",
|
|
570
|
+
hint: "the Worker's own binding — no API key to issue, store or rotate; Cloudflare writes the SPF, DKIM and DMARC records itself. Needs the domain onboarded once, and a Workers Paid plan to reach anyone but you",
|
|
571
|
+
packages: ["@saastemly/better-email"],
|
|
572
|
+
recommended: true,
|
|
573
|
+
env: [
|
|
574
|
+
{
|
|
575
|
+
key: "EMAIL_FROM",
|
|
576
|
+
breaks: "Cloudflare refuses a sender outside the onboarded domain",
|
|
577
|
+
plaintext: true,
|
|
578
|
+
where: "an address at a domain onboarded for Email Sending",
|
|
579
|
+
},
|
|
580
|
+
],
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
id: "resend",
|
|
584
|
+
label: "Resend",
|
|
585
|
+
hint: "an API key and a verified domain; the free tier is 3,000 a month. MailDev stands in locally either way",
|
|
586
|
+
packages: ["@saastemly/better-email"],
|
|
587
|
+
env: [
|
|
588
|
+
{ key: "RESEND_API_KEY", breaks: "no mail at all: nobody can sign in, no confirmations", where: "resend.com" },
|
|
589
|
+
{ key: "EMAIL_FROM", breaks: "Resend refuses an unverified sender", plaintext: true, where: "a domain verified at Resend" },
|
|
590
|
+
],
|
|
591
|
+
},
|
|
592
|
+
],
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
export const DNS: Group = {
|
|
596
|
+
id: "dns",
|
|
597
|
+
title: "DNS",
|
|
598
|
+
intro: "So the deploy writes the zone's records, not a person in a registrar's UI. The worker's own hostname is a Cloudflare custom domain and needs no record here; the mail sender's do.",
|
|
599
|
+
kind: "select",
|
|
600
|
+
choices: [
|
|
601
|
+
{
|
|
602
|
+
id: "cloudflare",
|
|
603
|
+
label: "Cloudflare",
|
|
604
|
+
hint: "the zone the shop's domain is in — the deploy writes the mail records so confirmations are not spam",
|
|
605
|
+
packages: ["@saastemly/better-dns"],
|
|
606
|
+
recommended: true,
|
|
607
|
+
env: [
|
|
608
|
+
{ key: "CLOUDFLARE_DNS_TOKEN", breaks: "the storefront records are never written; the domain does not reach the shop", where: "a token with Zone:DNS:Edit and Zone:Zone:Read" },
|
|
609
|
+
],
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
id: "simply",
|
|
613
|
+
label: "Simply.com",
|
|
614
|
+
hint: "for a zone that stays there — but then the worker cannot have a custom domain, so the shop has no hostname",
|
|
615
|
+
packages: ["@saastemly/better-dns"],
|
|
616
|
+
env: [{ key: "SIMPLY_API_KEY", breaks: "the storefront records are never written", where: "Simply control panel" }],
|
|
617
|
+
},
|
|
618
|
+
{ id: "none", label: "None", hint: "you write the mail records by hand, and a deploy does not mean mail is trusted" },
|
|
619
|
+
],
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
export const CONTENT: Group = {
|
|
623
|
+
id: "content",
|
|
624
|
+
title: "Content",
|
|
625
|
+
kind: "multiselect",
|
|
626
|
+
choices: [
|
|
627
|
+
{ id: "blogs", label: "Blog", hint: "posts with scheduling, drafts and a public slug", packages: ["@saastemly/better-blogs"], recommended: true },
|
|
628
|
+
{ id: "faqs", label: "FAQ", hint: "grouped, ordered, localized — with launch content you declare in code", packages: ["@saastemly/better-faqs"], recommended: true },
|
|
629
|
+
],
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
export const UI: Group = {
|
|
633
|
+
id: "ui",
|
|
634
|
+
title: "Interfaces",
|
|
635
|
+
kind: "multiselect",
|
|
636
|
+
choices: [
|
|
637
|
+
{
|
|
638
|
+
id: "admin",
|
|
639
|
+
label: "Generated storefront and panel",
|
|
640
|
+
hint: "better-admin-ui — every table becomes a screen at /api-dashboard, each caller seeing their own scope: a customer their orders, staff everything. Standard theming; one line to turn off once you have built your own",
|
|
641
|
+
packages: ["@saastemly/better-admin-ui"],
|
|
642
|
+
recommended: true,
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
id: "custom",
|
|
646
|
+
label: "Custom storefront kit",
|
|
647
|
+
hint: "better-auth-ui and better-commerce-ui — hooks, query options and shadcn components you host in your own app, for full freedom in theming and branding",
|
|
648
|
+
packages: ["@better-auth-ui/react", "@saastemly/better-commerce-ui"],
|
|
649
|
+
},
|
|
650
|
+
],
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
/** The order the wizard walks them in. */
|
|
654
|
+
export const GROUPS: Group[] = [
|
|
655
|
+
SIGN_IN,
|
|
656
|
+
AUTH_PLUGINS,
|
|
657
|
+
COMMERCE_CATALOG,
|
|
658
|
+
COMMERCE_SALES,
|
|
659
|
+
COMMERCE_OPERATIONS,
|
|
660
|
+
PAYMENT,
|
|
661
|
+
TAX,
|
|
662
|
+
CARRIERS,
|
|
663
|
+
ERP,
|
|
664
|
+
EMAIL,
|
|
665
|
+
DNS,
|
|
666
|
+
CONTENT,
|
|
667
|
+
UI,
|
|
668
|
+
];
|
|
669
|
+
|
|
670
|
+
/** Every choice, by id, for lookups the generator makes. */
|
|
671
|
+
export const CHOICES = new Map<string, Choice>(
|
|
672
|
+
GROUPS.flatMap((group) => group.choices.map((choice) => [choice.id, choice] as const)),
|
|
673
|
+
);
|