@redeemid/emdash 0.0.16 → 0.0.18

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.
Files changed (73) hide show
  1. package/README.md +447 -232
  2. package/bin/redeemid-emdash.mjs +13 -1
  3. package/docs/developer-api.md +126 -0
  4. package/docs/shopify-setup.md +1 -1
  5. package/package.json +3 -1
  6. package/src/access-admin.ts +754 -0
  7. package/src/access.ts +84 -18
  8. package/src/admin-api.ts +9 -2
  9. package/src/agents.ts +62 -3
  10. package/src/api-version.ts +6 -0
  11. package/src/blocks/CollectionBlock.astro +4 -1
  12. package/src/blocks/SearchBlock.astro +14 -0
  13. package/src/blocks/index.ts +2 -0
  14. package/src/blocks-names.ts +1 -0
  15. package/src/cart-ops.ts +164 -0
  16. package/src/catalog.ts +287 -0
  17. package/src/checkout.ts +73 -0
  18. package/src/client/redeemid.ts +563 -0
  19. package/src/components/AccessBadge.astro +7 -5
  20. package/src/components/Account.astro +1 -1
  21. package/src/components/BrowseControls.astro +87 -0
  22. package/src/components/Cart.astro +12 -6
  23. package/src/components/CartDrawer.astro +59 -0
  24. package/src/components/CartView.astro +2 -1
  25. package/src/components/Collection.astro +23 -18
  26. package/src/components/Content.astro +10 -5
  27. package/src/components/Contribution.astro +1 -1
  28. package/src/components/Gate.astro +4 -2
  29. package/src/components/LockedPanel.astro +5 -3
  30. package/src/components/MemberCard.astro +1 -1
  31. package/src/components/Notices.astro +3 -28
  32. package/src/components/Plan.astro +1 -1
  33. package/src/components/Preferences.astro +1 -1
  34. package/src/components/Product.astro +116 -25
  35. package/src/components/ProductGrid.astro +52 -0
  36. package/src/components/ProductSearch.astro +83 -0
  37. package/src/components/Recommendations.astro +45 -0
  38. package/src/components/RedeemIDClient.astro +45 -0
  39. package/src/components/YourData.astro +1 -1
  40. package/src/config.ts +31 -5
  41. package/src/contract.ts +50 -0
  42. package/src/descriptor.ts +7 -0
  43. package/src/events.ts +3 -0
  44. package/src/features.ts +1 -1
  45. package/src/files.ts +1 -1
  46. package/src/guard.ts +5 -3
  47. package/src/index.ts +3 -0
  48. package/src/log.ts +96 -0
  49. package/src/monitor.ts +560 -0
  50. package/src/notices.ts +29 -0
  51. package/src/plugin.ts +234 -88
  52. package/src/rate-limit.ts +7 -0
  53. package/src/routes/api/cart.ts +84 -0
  54. package/src/routes/api/search.ts +49 -0
  55. package/src/routes/api/shared.ts +4 -0
  56. package/src/routes/api/state.ts +69 -0
  57. package/src/routes/buy.ts +4 -3
  58. package/src/routes/callback.ts +23 -5
  59. package/src/routes/cart-edit.ts +15 -66
  60. package/src/routes/cart.ts +4 -2
  61. package/src/routes/preferences.ts +2 -2
  62. package/src/routes/privacy.ts +2 -2
  63. package/src/routes/shared.ts +11 -0
  64. package/src/routes/webhook.ts +24 -11
  65. package/src/settings.ts +195 -0
  66. package/src/shopify.ts +37 -1
  67. package/src/site-config.ts +38 -5
  68. package/src/staff.ts +12 -23
  69. package/src/storage.ts +12 -0
  70. package/src/storefront.ts +231 -59
  71. package/src/text.ts +43 -2
  72. package/src/version.ts +1 -1
  73. package/src/webhooks.ts +2 -1
@@ -27,6 +27,9 @@ Commands:
27
27
  member <id or email> One synced member
28
28
  health What's set up (never the values)
29
29
  changes --since <date> Who joined or left a level [--level <slug>]
30
+ products <words> Search the Shopify catalog for handles and prices
31
+ product <handle> One product: options, variants, selling plans
32
+ collection <handle> One collection: its products and filters
30
33
  resync <customer id> Read one member from Shopify again *
31
34
  resync-batch Read the 25 members synced longest ago *
32
35
  tags <customer id> Edit tags in Shopify: --add a,b --remove c *
@@ -82,6 +85,15 @@ function request() {
82
85
  case "changes":
83
86
  if (!o.since) throw new Error("changes needs --since <date>");
84
87
  return ["level-changes", { since: o.since, level: o.level }];
88
+ case "products":
89
+ if (!arg) throw new Error("products needs search words");
90
+ return ["find-products", { query: positionals.slice(1).join(" ") }];
91
+ case "product":
92
+ if (!arg) throw new Error("product needs a handle");
93
+ return ["get-product", { handle: arg }];
94
+ case "collection":
95
+ if (!arg) throw new Error("collection needs a handle");
96
+ return ["get-collection", { handle: arg }];
85
97
  case "resync":
86
98
  if (!arg) throw new Error("resync needs a customer id");
87
99
  return ["resync-member", { customerId: arg }];
@@ -131,7 +143,7 @@ async function main() {
131
143
  } catch {
132
144
  throw new Error(`${res.status} from the site: ${text.slice(0, 200)}`);
133
145
  }
134
- if (res.status === 404) throw new Error(`The site has no "${route}" route. Actions need agents: { actions: true }, and every command needs @redeemid/emdash 0.0.11 or later.`);
146
+ if (res.status === 404) throw new Error(`The site has no "${route}" route. Actions need agents: { actions: true }, and every command needs @redeemid/emdash 0.0.11 or later (products, product and collection need 0.0.18).`);
135
147
  if (!res.ok) throw new Error(json?.error?.message ?? `${res.status} from the site`);
136
148
  const out = json?.data ?? json;
137
149
  if (out && out.ok === false) throw new Error(out.error);
@@ -0,0 +1,126 @@
1
+ # Developer API
2
+
3
+ What site code, other plugins and AI agents can rely on in `@redeemid/emdash`. API version **1.0.0** (`API_VERSION` in `@redeemid/emdash/contract`, also in `api/state` and `window.redeemid.apiVersion`).
4
+
5
+ Everything on this page is stable: it changes only with a changelog entry and an API version bump (minor when something is added, major when something changes or goes). Anything not listed is internal, even when a module exports it. `src/contract.ts` is the same list as code, and a test fails when the code, that list and this page disagree.
6
+
7
+ Three rules hold everywhere:
8
+
9
+ - **Shopify decides.** Levels and features come only from the member's Shopify tags; prices, stock, discounts and checkout come only from Shopify. Nothing here grants access or sets a price.
10
+ - **Nothing personal leaves the server.** Browser-facing answers and events never carry an email, a name, a customer id or a Shopify tag. Match people by `customerId` only in signed server events.
11
+ - **Vendor-agnostic.** RedeemID never sends anything to an analytics tool, CRM or tag manager. You subscribe and forward what you choose.
12
+
13
+ ## Server modules
14
+
15
+ Import these in Astro pages, components and endpoints.
16
+
17
+ | Import | Stable exports |
18
+ | --- | --- |
19
+ | `@redeemid/emdash` | `redeemid`, `redeemidGuard`, `redeemidPlugin` |
20
+ | `@redeemid/emdash/access` | `viewer`, `canRead`, `readEntry`, `entryRequirement`, `meterUsage`, `membership`, `levelsFor`, `signInUrl`, `signOutUrl`, `accountUrl`, `cartUrl`, `refreshUrl`, `safeReturn` |
21
+ | `@redeemid/emdash/features` | `hasFeature`, `viewerFeatures`, `featuresFor` |
22
+ | `@redeemid/emdash/preferences` | `readPreferences`, `savePreferences`, `preferenceFields` |
23
+ | `@redeemid/emdash/storefront` | `plan`, `product`, `collection`, `money`, `realOptions`, `variantFor`, `storeContext`, `cartDetails`, `addToCart`, `updateLines`, `removeLines`, `setDiscountCodes` |
24
+ | `@redeemid/emdash/catalog` | `searchProducts`, `collectionPage`, `suggest`, `recommendations`, `browseFromUrl`, `browseHref`, `parseFilters`, `priceFilter`, `SORTS` |
25
+ | `@redeemid/emdash/events` | `signBody` |
26
+ | `@redeemid/emdash/text` | `STRINGS`, `text`, `textParts` |
27
+
28
+ The site's config for a request comes from `siteConfig(Astro.locals)` (code config with the admin's saved settings laid over it). A few examples:
29
+
30
+ ```astro
31
+ ---
32
+ import { canRead, viewer } from "@redeemid/emdash/access";
33
+ import { hasFeature } from "@redeemid/emdash/features";
34
+ import { searchProducts } from "@redeemid/emdash/catalog";
35
+
36
+ const verdict = await canRead(Astro, { collection: "posts", terms: post.data.terms, id: post.id });
37
+ const adFree = await hasFeature(Astro, "ad-free");
38
+ ---
39
+ ```
40
+
41
+ Catalog reads (`product`, `collection`, `searchProducts`, `collectionPage`, `suggest`, `recommendations`) are cached for five minutes per Worker isolate. Pass `storeContext(config, Astro.request)` as `context` for local prices when config `market` is set.
42
+
43
+ ## JSON routes
44
+
45
+ Under `/_emdash/api/auth/redeemid/`. Same-origin only: EmDash checks `Origin` on every POST, and the browser client also sends `X-EmDash-Request: 1`. Member answers are `Cache-Control: private, no-store`.
46
+
47
+ - `api/state` (GET): the visitor's own state.
48
+
49
+ ```json
50
+ { "apiVersion": "1.0.0", "version": "0.0.18", "signedIn": true, "linked": true,
51
+ "levels": ["gold"], "features": ["ad-free"], "cart": { "count": 2 },
52
+ "meter": { "used": 1, "limit": 3, "period": "month" } }
53
+ ```
54
+
55
+ - `api/cart` (GET, POST): the Shopify cart. GET answers `{ cart }` (null without one). POST takes `{ action, ... }`:
56
+ - `add`: `product` (handle), then `variant` (an id of that product's) or `options` (`{ "Size": "M" }`), optional `sellingPlan` (one of the product's, or `""` for one-time), `quantity` (1 to 1000);
57
+ - `update`: `line`, `quantity` (0 removes); `remove`: `line`; `discount`: `code` (`""` clears);
58
+ - `checkout`: answers `{ checkoutUrl }`, signed in for members.
59
+
60
+ Other actions answer `{ notice, cart, added? }`. An add Shopify can't fill answers 422 with `notice: "plan_unavailable"`. Over the rate limit (with `redeemidGuard({ rateLimit: true })`) POST answers 429.
61
+ - `api/search` (GET): public catalog data, `Cache-Control: public, max-age=60` (private when `market.country` is `"visitor"`).
62
+ - `?q=tote&suggest=1[&limit=6]`: `{ queries, products, collections }`, for search-as-you-type;
63
+ - `?q=tote`: `{ products, filters, pageInfo, total }`;
64
+ - `?collection=shop`: the collection's page, `{ handle, title, description, products, filters, pageInfo }`.
65
+
66
+ Both take `first` (1 to 50), `sort`, `after` or `before` (cursors from `pageInfo`), `filter` (repeatable: a filter value's `input` exactly as Shopify gave it), `price_min` and `price_max`. Sorts are `relevance`, `best-selling`, `newest`, `price-asc`, `price-desc`, `title-asc`, `title-desc` (search: relevance and price only).
67
+ - `preferences` (GET, POST) and `privacy` (GET export, POST erase): see the README.
68
+
69
+ Form routes, which redirect back with `?redeemid_notice=<code>`: `login`, `callback`, `logout`, `webhook`, `buy`, `cart`, `cart-edit`, `account`.
70
+
71
+ ## Browser client
72
+
73
+ Add `<RedeemIDClient />` once to the site layout (from `@redeemid/emdash/components/RedeemIDClient.astro`). It defines `window.redeemid` and sends nothing anywhere by itself.
74
+
75
+ ```js
76
+ const stop = window.redeemid.subscribe((name, props, event) => {
77
+ // event: { name, version, time, props }, props are scalars only
78
+ });
79
+ ```
80
+
81
+ A subscriber also gets the events that fired before it subscribed (`{ replay: false }` skips them), so it can wait for consent. Each event is also dispatched on `document` as a `CustomEvent` named `redeemid:<name>`.
82
+
83
+ | Member | What it does |
84
+ | --- | --- |
85
+ | `version`, `apiVersion` | Package and API versions |
86
+ | `events` | The event names below |
87
+ | `subscribe(fn, { replay })` | Listen; returns an unsubscribe function |
88
+ | `emit(name, props)` | Send your own event; names must start `site_` |
89
+ | `state()`, `refresh()` | The visitor's `api/state`, read once (or again) |
90
+ | `signIn(returnTo?)`, `signOut(returnTo?)` | Go to sign-in; sign out (a POST) |
91
+ | `cart` | `get()`, `add({ product, variant?, options?, sellingPlan?, quantity? })`, `update(line, quantity)`, `remove(line)`, `discount(code)`, `checkout()` |
92
+ | `openCart()`, `closeCart()` | The cart drawer, when `<CartDrawer />` is on the page |
93
+ | `search(q, { first, after, sort })`, `suggest(q, limit)`, `collection(handle, { first, after, sort })` | The `api/search` answers |
94
+
95
+ Events:
96
+
97
+ | Event | Props |
98
+ | --- | --- |
99
+ | `ready` | `api_version` |
100
+ | `state` | `signedIn`, `linked`, `levels` and `features` (comma-separated slugs), `cartCount` |
101
+ | `wall_shown` | `entry`, `level`, `reason` |
102
+ | `metered_view` | `left` |
103
+ | `sign_in_click` | `source` (`page`, `wall`, `drawer`) |
104
+ | `checkout_click` | `source`, `product` |
105
+ | `cart_changed` | `action`, `notice`, `count`, `product`, `quantity`, `subscription` |
106
+ | `cart_opened` | `source` (`link`, `add`, `api`) |
107
+ | `search` | `query_length` (never the words) |
108
+
109
+ The client also enhances RedeemID's markup: with `<CartDrawer />` on the page, Add to cart updates the cart in place and opens the drawer, and `<Cart drawer />` opens it. Product option pickers keep the price, sale price, image and stock in step, and `<ProductSearch>` suggests as you type. Without JavaScript every form still works.
110
+
111
+ ## Server events
112
+
113
+ Signed outbound webhooks to config `events.url` (see the README): `member.signed_in`, `member.levels_changed`, `meter.used`, `meter.exhausted`, `preferences.saved`, `member.erased`. Verify `X-RedeemID-Signature` with `signBody(rawBody, secret)`.
114
+
115
+ ## MCP and the command line
116
+
117
+ RedeemID's tools join EmDash's MCP server once staff turn them on for RedeemID under Plugins. They need a staff token (permission `users:read`). Always offered, read-only:
118
+
119
+ - `explain_access`, `list_levels`, `get_rules`, `get_member`, `site_health`, `level_changes`
120
+ - `find_products`, `get_product`, `get_collection`: the Shopify catalog the site sells, so an agent writing content can find the right product and collection handles.
121
+
122
+ Only with config `agents: { actions: true }`: `resync_member`, `resync_batch`, `edit_member_tags`.
123
+
124
+ The `redeemid-emdash` command line calls the same routes (`npx redeemid-emdash --help`).
125
+
126
+ For shoppers' own agents (browsing products, building a cart, checking out), point them at Shopify's Storefront MCP server for the store, `https://<store domain>/api/mcp`. It runs on Shopify, where checkout and prices already live, so RedeemID doesn't repeat it.
@@ -35,7 +35,7 @@ Shopify's compliance topics (`customers/data_request`, `customers/redact`, `shop
35
35
 
36
36
  ## 3. The site
37
37
 
38
- Add the package and config as in the [README](../README.md#install), with `shop` set to the store's domain. A custom customer-account domain works as well as `*.myshopify.com`.
38
+ Add the package and config as in the [README](../README.md#quick-start), with `shop` set to the store's domain. A custom customer-account domain works as well as `*.myshopify.com`.
39
39
 
40
40
  Set the secrets. On Cloudflare Workers:
41
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redeemid/emdash",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "description": "Shopify customer accounts, member levels and paywalls for EmDash sites. Shopify runs commerce; EmDash runs content.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -16,10 +16,12 @@
16
16
  "./guard.ts": "./src/guard.ts",
17
17
  "./rate-limit-cloudflare.ts": "./src/rate-limit-cloudflare.ts",
18
18
  "./storefront": "./src/storefront.ts",
19
+ "./catalog": "./src/catalog.ts",
19
20
  "./features": "./src/features.ts",
20
21
  "./events": "./src/events.ts",
21
22
  "./preferences": "./src/preferences.ts",
22
23
  "./text": "./src/text.ts",
24
+ "./contract": "./src/contract.ts",
23
25
  "./plugin": "./src/plugin.ts",
24
26
  "./blocks": "./src/blocks/index.ts",
25
27
  "./components/*": "./src/components/*",