arky-sdk 0.9.20 → 0.10.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 CHANGED
@@ -1,8 +1,6 @@
1
1
  # arky-sdk
2
2
 
3
- Official TypeScript SDK for [Arky](https://arky.io), the website backend and client Admin for custom frontends.
4
-
5
- Arky lets you keep frontend control while using one backend for CMS, commerce, bookings, forms, profiles, action, experiments, support, workflows, API, and SDK.
3
+ Official TypeScript SDK for [Arky](https://arky.io), the website backend and Admin client for custom frontends.
6
4
 
7
5
  ## Installation
8
6
 
@@ -10,114 +8,75 @@ Arky lets you keep frontend control while using one backend for CMS, commerce, b
10
8
  npm install arky-sdk
11
9
  ```
12
10
 
13
- ## Storefront Quick Start
11
+ ## Storefront quick start
14
12
 
15
- Use `initialize` from `arky-sdk/storefront` for normal custom frontends. It creates the Arky storefront object, keeps store/locale/market context, and exposes the backend modules the frontend needs.
13
+ Copy the Store publishable key from Developer and initialize one client:
16
14
 
17
15
  ```typescript
18
- import { initialize } from "arky-sdk/storefront";
19
-
20
- const arky = initialize({
21
- baseUrl: "https://api.arky.io",
22
- storeId: "your-store-id",
23
- market: "us",
24
- locale: "en",
25
- marketForLocale: (locale) => (locale === "it" ? "ita" : "us"),
26
- });
27
-
28
- const homepage = await arky.cms.entry.get({
29
- collection_id: "pages",
30
- key: "homepage",
31
- locale: "en",
32
- });
16
+ import { initialize } from "arky-sdk";
33
17
 
34
- await arky.action.track({
35
- key: "page.view",
36
- payload: { path: location.pathname },
37
- });
18
+ export const arky = initialize(
19
+ import.meta.env.PUBLIC_ARKY_PUBLISHABLE_KEY,
20
+ );
38
21
  ```
39
22
 
40
- `initialize` is the public storefront factory. Use the Arky domain noun `Store` for tenant/business concepts, not for naming the SDK factory.
23
+ `initialize` is synchronous. It makes no request and creates no visitor. Production requests use `https://api.arky.io` by default.
41
24
 
42
- ## Storefront Modules
43
-
44
- The storefront module API is the preferred surface for websites:
25
+ For local development or an explicit initial context:
45
26
 
46
27
  ```typescript
47
- await arky.eshop.cart.load();
48
-
49
- await arky.cms.entry.get({
50
- collection_id: "pages",
51
- key: "homepage",
52
- locale: "en",
53
- });
54
- await arky.cms.form.submitByKey({ key: "contact", values: {} });
55
-
56
- const { items: products } = await arky.eshop.product.list({ limit: 20 });
57
- await arky.eshop.cart.addProduct(products[0], products[0].variants[0], 1);
58
- await arky.eshop.cart.quote();
59
- await arky.eshop.cart.checkout({ payment_method_key: "cash" });
60
-
61
- const { items: services } = await arky.eshop.service.list({ limit: 20 });
62
- await arky.eshop.service.initialize();
63
-
64
- await arky.action.track({
65
- key: "project.inquiry.started",
66
- payload: { placement: "homepage" },
28
+ export const arky = initialize("arky_pk_...", {
29
+ apiUrl: "http://localhost:8000",
30
+ locale: "it",
31
+ market: "ita",
67
32
  });
68
33
  ```
69
34
 
70
- UI frameworks can subscribe to Nano Stores exposed by the modules:
35
+ The SDK accepts only an `arky_pk_...` publishable key. A personal `arky_api_...` token or an `arky_vst_...` visitor session is rejected at initialization. Publishable keys identify a Store; they grant no Admin access and are safe to include in browser code.
71
36
 
72
- ```typescript
73
- const unsubscribe = arky.eshop.cart.snapshot.subscribe((snapshot) => {
74
- console.log(snapshot.item_count, snapshot.cart?.id);
75
- });
37
+ ## Read content and submit forms
76
38
 
77
- await arky.eshop.cart.load();
78
- unsubscribe();
79
- ```
80
-
81
- ## Read Content And Submit Forms
82
-
83
- The server storefront entry route uses collection entries. Key-based content loads must include `collection_id`.
39
+ Anonymous CMS and catalog reads do not create a visitor:
84
40
 
85
41
  ```typescript
86
42
  const page = await arky.cms.entry.get({
87
43
  collection_id: "pages",
88
44
  key: "homepage",
89
- locale: "en",
90
45
  });
91
46
 
92
47
  const titleBlock = page.blocks.find((block) => block.key === "title");
93
- const title = arky.utils.getBlockTextValue(titleBlock, "en");
48
+ const title = arky.utils.getBlockTextValue(titleBlock, arky.getLocale());
49
+ ```
50
+
51
+ Stateful operations identify the visitor lazily. Concurrent first operations share one identify request:
94
52
 
53
+ ```typescript
95
54
  await arky.cms.form.submitByKey({
96
55
  key: "contact",
97
56
  values: {
98
- email: "profile@example.com",
57
+ email: "visitor@example.com",
99
58
  message: "Hello from the storefront",
100
59
  },
101
60
  });
102
61
  ```
103
62
 
104
- ## Browse Products And Checkout
63
+ The browser persists only the `arky_vst_...` visitor-session token. Storage is isolated by API endpoint and a fingerprint of the publishable key.
64
+
65
+ ## Products, services, and checkout
105
66
 
106
67
  ```typescript
107
68
  const { items: products } = await arky.eshop.product.list({ limit: 20 });
108
69
  const product = await arky.eshop.product.get({ id: products[0].id });
109
- const variant = product.variants[0];
110
-
111
- await arky.eshop.cart.addProduct(product, variant, 2);
112
70
 
113
- const quote = await arky.eshop.cart.quote();
71
+ await arky.eshop.cart.addProduct(product, product.variants[0], 2);
72
+ await arky.eshop.cart.quote();
114
73
 
115
74
  const order = await arky.eshop.cart.checkout({
116
- payment_method_key: "credit_card",
75
+ payment_method_key: "cash",
117
76
  });
118
77
  ```
119
78
 
120
- ## Sell Scheduled Services
79
+ Scheduled services use the same cart:
121
80
 
122
81
  ```typescript
123
82
  const { items: services } = await arky.eshop.service.list({ limit: 20 });
@@ -129,68 +88,175 @@ arky.eshop.service.findFirstAvailable();
129
88
  const state = arky.eshop.service.state.get();
130
89
  if (state.slots[0]) {
131
90
  arky.eshop.service.selectTimeSlot(state.slots[0]);
132
- const configuredForms = arky.eshop.service.form_groups.get();
133
- // Render and bind each configuredForms[i].blocks before adding the booking.
134
91
  arky.eshop.service.nextStep();
135
92
  await arky.eshop.service.addToCart();
136
93
  }
94
+ ```
137
95
 
138
- await arky.eshop.cart.checkout({
139
- payment_method_key: "cash",
96
+ Nano Stores expose reactive module state:
97
+
98
+ ```typescript
99
+ const unsubscribe = arky.eshop.cart.snapshot.subscribe((snapshot) => {
100
+ console.log(snapshot.item_count, snapshot.cart?.id);
140
101
  });
102
+
103
+ await arky.eshop.cart.load();
104
+ unsubscribe();
141
105
  ```
142
106
 
143
- Booking forms are resolved from the selected service-provider relation by exact form ID. A provider with no configured forms sends `[]`. For a longer booking, pass an explicit list of adjacent slots for the same service and provider to `addToCart(slots)`.
107
+ ## Locale and market context
144
108
 
145
- ## Low-Level Client
109
+ Locale and market are independent. Neither is inferred from the other, browser language, IP address, or geolocation:
146
110
 
147
- The lower-level SDK client remains available as `arky.client` for admin tools and advanced frontend utilities. Normal storefronts should use the module API first.
111
+ ```typescript
112
+ arky.setContext({ locale: "bs" });
113
+ arky.setContext({ market: "bih" });
114
+ ```
115
+
116
+ Use an isolated scoped client for SSR, static generation, or parallel contexts:
148
117
 
149
118
  ```typescript
150
- const sdk = arky.client;
119
+ const italian = arky.withContext({ locale: "it", market: "ita" });
120
+ const page = await italian.cms.entry.get({
121
+ collection_id: "pages",
122
+ key: "homepage",
123
+ });
124
+ ```
125
+
126
+ Changing the scoped client does not mutate the original client. A market change while the cart contains items throws `CART_MARKET_LOCKED`; the SDK never silently clears or reprices the cart.
127
+
128
+ ## Store setup and Stripe
129
+
130
+ Store setup is fetched lazily and deduplicated:
131
+
132
+ ```typescript
133
+ const setup = await arky.store.load();
134
+ console.log(setup.languages.default, setup.markets.default);
135
+ ```
136
+
137
+ Payment configuration belongs to Arky. Mounting card payment waits for setup internally and accepts no Stripe publishable key or connected Account ID:
138
+
139
+ ```typescript
140
+ await arky.eshop.cart.payment.mountStripe("#payment", {
141
+ appearance: { theme: "stripe" },
142
+ });
143
+ ```
144
+
145
+ For a paid flow outside the cart, pass only customer-facing amount and currency:
146
+
147
+ ```typescript
148
+ await arky.eshop.cart.payment.mountStripe("#payment", {
149
+ amount: 2500,
150
+ currency: "EUR",
151
+ });
152
+ ```
153
+
154
+ ## SSR and static generation
155
+
156
+ Anonymous reads work without browser storage. Stateful SSR requires an explicit request-local adapter so a server module cannot retain one visitor across requests:
157
+
158
+ ```typescript
159
+ const arky = initialize(process.env.ARKY_PUBLISHABLE_KEY!, {
160
+ locale: requestLocale,
161
+ market: requestMarket,
162
+ sessionStorage: {
163
+ getItem: (key) => requestSession.get(key) ?? null,
164
+ setItem: (key, value) => requestSession.set(key, value),
165
+ removeItem: (key) => requestSession.delete(key),
166
+ },
167
+ });
168
+ ```
169
+
170
+ Create one client per request. `withContext` also creates an isolated visitor session; when used during SSR it reuses the request-local adapter under a separate scoped storage key. The SDK does not ship framework-specific cookie adapters.
171
+
172
+ ## Low-level storefront client
151
173
 
152
- await sdk.eshop.product.find({ limit: 20 });
153
- await sdk.eshop.cart.current({ market: arky.getMarket() });
154
- await sdk.eshop.order.find({});
155
- await sdk.cms.entry.find({
174
+ The module facade exposes its low-level client as `arky.client`:
175
+
176
+ ```typescript
177
+ await arky.client.eshop.product.find({ limit: 20 });
178
+ await arky.client.eshop.cart.current();
179
+ await arky.client.cms.entry.find({
156
180
  collection_id: "pages",
157
181
  key: "homepage",
158
182
  limit: 1,
159
183
  });
160
184
  ```
161
185
 
162
- ## Configuration Options
186
+ Low-level requests use Store-ID-free `/v1/storefront` routes and send connection context as headers:
163
187
 
164
- ```typescript
165
- initialize({
166
- // Required
167
- baseUrl: string,
168
- storeId: string,
188
+ ```http
189
+ X-Arky-Publishable-Key: arky_pk_...
190
+ X-Arky-Locale: it
191
+ X-Arky-Market: ita
192
+ Authorization: Bearer arky_vst_...
193
+ ```
169
194
 
170
- // Optional
171
- market?: string,
195
+ Locale and market headers are omitted when no explicit context is set, allowing the server to use Store defaults.
196
+
197
+ ## Configuration
198
+
199
+ ```typescript
200
+ initialize(publishableKey: string, {
201
+ apiUrl?: string,
172
202
  locale?: string,
173
- apiToken?: string,
174
- marketForLocale?: (locale: string) => string | null,
175
- navigate?: (path: string) => void,
176
- loginFallbackPath?: string,
203
+ market?: string,
204
+ sessionStorage?: StorefrontSessionStorage,
205
+ });
206
+ ```
207
+
208
+ One storefront client always represents one publishable key and one Store. To connect to another Store, initialize a second explicit client with its publishable key.
209
+
210
+ ## Admin client
211
+
212
+ Private operator integrations use the separate Admin client. Personal API tokens must never be exposed in browser code:
213
+
214
+ ```typescript
215
+ import { createAdmin } from "arky-sdk/admin";
216
+
217
+ const admin = createAdmin({
218
+ baseUrl: "https://api.arky.io",
219
+ storeId: "internal-store-id",
220
+ apiToken: process.env.ARKY_PERSONAL_API_TOKEN,
177
221
  });
178
222
  ```
179
223
 
180
- ## TypeScript Support
224
+ Store connection management is available through the Admin surface:
181
225
 
182
226
  ```typescript
183
- import { initialize, type ArkyStore } from "arky-sdk/storefront";
184
- import type { Block, Cart, Order, Price, Product, Service, Store } from "arky-sdk";
227
+ const store = await admin.store.regeneratePublishableKey({
228
+ store_id: "internal-store-id",
229
+ });
230
+
231
+ await admin.store.update({
232
+ id: store.id,
233
+ default_market_id: "market-id",
234
+ });
235
+ ```
236
+
237
+ ## TypeScript
238
+
239
+ ```typescript
240
+ import {
241
+ initialize,
242
+ type ArkyStore,
243
+ type StorefrontDto,
244
+ type StorefrontSetup,
245
+ } from "arky-sdk";
246
+ import type { Block, Cart, Order, Price, Product, Service } from "arky-sdk";
247
+
248
+ type StorefrontProduct = StorefrontDto<Product>;
249
+ type StorefrontCart = StorefrontDto<Cart>;
185
250
  ```
186
251
 
187
- ## Adding A New Endpoint
252
+ Storefront request types intentionally contain no Store routing ID. Admin request types remain Store-explicit.
253
+
254
+ ## Adding an endpoint
188
255
 
189
- When adding a new SDK method, follow this checklist so the API surface stays typed end-to-end and request shapes stay aligned with the Rust DTOs on the server:
256
+ When adding SDK methods:
190
257
 
191
- 1. Define the entity in `src/types/index.ts` if it does not already exist. Mirror the Rust response struct field-for-field.
192
- 2. Define the request params in `src/types/api.ts`. Mirror the Rust DTO in `server/core/src/{module}/types/commands.rs` field-for-field. Two exceptions: `store_id?: string` and `market?: string` are optional in TS because the SDK auto-fills both from `apiConfig.storeId` and `apiConfig.market`. Do not use `[key: string]: any` index signatures.
193
- 3. Annotate the SDK method's return type using the matching entity from `src/types/index.ts`.
194
- 4. Pass the response generic to the HTTP call: `apiConfig.httpClient.post<EntityType>(...)`, `apiConfig.httpClient.get<PaginatedResponse<EntityType>>(...)`, etc. Never rely on inference.
195
- 5. Inject `market` from `apiConfig`: when a body needs a `market` field, write `{ market: apiConfig.market, ...payload }`. Never hardcode `"default"`, `"eshop"`, or any other market string in `src/api/*.ts`.
196
- 6. Re-export the entity from `src/index.ts` if consumers will import it.
258
+ 1. Mirror server response DTOs in `src/types/index.ts` or the relevant API module.
259
+ 2. Keep Admin inputs Store-explicit, but omit `store_id` from every storefront input, URL, and body.
260
+ 3. Use `/v1/storefront/...` keyless routes and let the shared client attach publishable-key, locale, market, and visitor headers.
261
+ 4. Mark customer mutations as stateful so they call the deduplicated visitor-session lifecycle.
262
+ 5. Add explicit response generics to every HTTP call and re-export consumer-facing types.
package/dist/admin.cjs CHANGED
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ require('nanostores');
4
+
3
5
  // src/utils/queryParams.ts
4
6
  function buildQueryString(params) {
5
7
  const queryParts = Object.entries(params).flatMap(
@@ -27,6 +29,15 @@ function requestError(name, message, details = {}) {
27
29
  function isRecord(value) {
28
30
  return typeof value === "object" && value !== null;
29
31
  }
32
+ function omitStorefrontRouting(value) {
33
+ if (!isRecord(value) || Array.isArray(value)) return value;
34
+ const prototype = Object.getPrototypeOf(value);
35
+ if (prototype !== Object.prototype && prototype !== null) return value;
36
+ const result = { ...value };
37
+ delete result.store_id;
38
+ delete result.market;
39
+ return result;
40
+ }
30
41
  function isTokenSet(value) {
31
42
  return isRecord(value) && typeof value.access_token === "string" && (value.refresh_token === void 0 || typeof value.refresh_token === "string") && (value.access_expires_at === void 0 || typeof value.access_expires_at === "number");
32
43
  }
@@ -102,10 +113,25 @@ function createHttpClient(cfg) {
102
113
  if (!retried && options?.transformRequest) {
103
114
  body = options.transformRequest(body);
104
115
  }
116
+ if (cfg.storefrontMode) {
117
+ body = omitStorefrontRouting(body);
118
+ }
119
+ const forcedHeaders = typeof cfg.forcedHeaders === "function" ? cfg.forcedHeaders() : cfg.forcedHeaders || {};
120
+ const callerHeaders = { ...options?.headers || {} };
121
+ const protectedHeaderNames = new Set(
122
+ Object.keys(forcedHeaders).map((name) => name.toLowerCase())
123
+ );
124
+ if (cfg.storefrontMode) protectedHeaderNames.add("authorization");
125
+ for (const name of Object.keys(callerHeaders)) {
126
+ if (protectedHeaderNames.has(name.toLowerCase())) {
127
+ delete callerHeaders[name];
128
+ }
129
+ }
105
130
  const headers = {
106
131
  Accept: "application/json",
107
132
  "Content-Type": "application/json",
108
- ...options?.headers || {}
133
+ ...callerHeaders,
134
+ ...forcedHeaders
109
135
  };
110
136
  let tokens = authStorage.getTokens();
111
137
  const nowSec = Date.now() / 1e3;
@@ -116,7 +142,8 @@ function createHttpClient(cfg) {
116
142
  if (tokens?.access_token) {
117
143
  headers["Authorization"] = `Bearer ${tokens.access_token}`;
118
144
  }
119
- const finalPath = options?.params ? path + buildQueryString(options.params) : path;
145
+ const requestParams = cfg.storefrontMode ? omitStorefrontRouting(options?.params) : options?.params;
146
+ const finalPath = requestParams ? path + buildQueryString(requestParams) : path;
120
147
  const fetchOptions = {
121
148
  method,
122
149
  headers,
@@ -147,8 +174,19 @@ function createHttpClient(cfg) {
147
174
  throw err;
148
175
  }
149
176
  if (res.status === 401 && !retried) {
150
- await ensureFreshToken();
151
- return request(method, path, body, options, true);
177
+ if (cfg.onUnauthorized) {
178
+ const recovered = await cfg.onUnauthorized({
179
+ hadAuthorization: Boolean(tokens?.access_token),
180
+ authorizationToken: tokens?.access_token || null,
181
+ path
182
+ });
183
+ if (recovered) {
184
+ return request(method, path, body, options, true);
185
+ }
186
+ } else {
187
+ await ensureFreshToken();
188
+ return request(method, path, body, options, true);
189
+ }
152
190
  }
153
191
  try {
154
192
  const contentLength = res.headers.get("content-length");
@@ -364,6 +402,14 @@ var createStoreApi = (apiConfig, _updateSession) => {
364
402
  params
365
403
  });
366
404
  },
405
+ async regeneratePublishableKey(params = {}, options) {
406
+ const store_id = params.store_id || apiConfig.storeId;
407
+ return apiConfig.httpClient.post(
408
+ `/v1/stores/${store_id}/publishable-key/regenerate`,
409
+ {},
410
+ options
411
+ );
412
+ },
367
413
  async getSubscriptionPlans(_params, options) {
368
414
  return apiConfig.httpClient.get("/v1/stores/plans", options);
369
415
  },
@@ -1262,7 +1308,10 @@ var createMarketApi = (apiConfig) => {
1262
1308
  async delete(params, options) {
1263
1309
  return apiConfig.httpClient.delete(
1264
1310
  `/v1/stores/${apiConfig.storeId}/markets/${params.id}`,
1265
- options
1311
+ {
1312
+ ...options,
1313
+ params: params.replacement_default_market_id ? { replacement_default_market_id: params.replacement_default_market_id } : options?.params
1314
+ }
1266
1315
  );
1267
1316
  }
1268
1317
  };
@@ -3508,6 +3557,7 @@ function createAdmin(config) {
3508
3557
  update: storeApi.updateStore,
3509
3558
  get: storeApi.getStore,
3510
3559
  find: storeApi.getStores,
3560
+ regeneratePublishableKey: storeApi.regeneratePublishableKey,
3511
3561
  subscription: {
3512
3562
  get: storeApi.getSubscription,
3513
3563
  getPlans: storeApi.getSubscriptionPlans,