@sixtyninecommerce/admin 0.6.0 → 0.8.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.
Files changed (39) hide show
  1. package/README.md +359 -2
  2. package/dist/apps/assertion.d.ts +30 -3
  3. package/dist/apps/assertion.js +40 -3
  4. package/dist/apps/config.d.ts +39 -0
  5. package/dist/apps/config.js +22 -0
  6. package/dist/apps/credential.d.ts +5 -0
  7. package/dist/apps/credential.js +55 -4
  8. package/dist/apps/index.d.ts +11 -0
  9. package/dist/client.d.ts +54 -1
  10. package/dist/client.js +34 -1
  11. package/dist/custom-objects/index.d.ts +66 -0
  12. package/dist/custom-objects/index.js +48 -0
  13. package/dist/customers/index.d.ts +225 -0
  14. package/dist/customers/index.js +279 -0
  15. package/dist/documents/index.d.ts +432 -0
  16. package/dist/documents/index.js +289 -0
  17. package/dist/index.d.ts +34 -2
  18. package/dist/index.js +16 -1
  19. package/dist/jobs/index.d.ts +143 -0
  20. package/dist/jobs/index.js +132 -0
  21. package/dist/metafields/index.d.ts +291 -0
  22. package/dist/metafields/index.js +141 -0
  23. package/dist/orders/index.d.ts +106 -0
  24. package/dist/orders/index.js +115 -0
  25. package/dist/orders/surcharges.d.ts +121 -0
  26. package/dist/orders/surcharges.js +110 -0
  27. package/dist/payments/index.d.ts +244 -0
  28. package/dist/payments/index.js +103 -0
  29. package/dist/payments/providers.d.ts +281 -0
  30. package/dist/payments/providers.js +205 -0
  31. package/dist/platform/provision-store.d.ts +9 -26
  32. package/dist/platform/provision-store.js +8 -66
  33. package/dist/tax/index.d.ts +128 -0
  34. package/dist/tax/index.js +101 -0
  35. package/dist/tenant/capabilities.d.ts +15 -0
  36. package/dist/types.d.ts +20 -3
  37. package/dist/uploads/index.d.ts +146 -0
  38. package/dist/uploads/index.js +72 -0
  39. package/package.json +3 -3
@@ -1,5 +1,5 @@
1
- import { errorFromResponse, TransportError } from '@sixtyninecommerce/core';
2
- import { importAppPrivateKey, signAppAssertion, } from './assertion.js';
1
+ import { errorFromResponse, RateLimitedError, TransportError } from '@sixtyninecommerce/core';
2
+ import { assertionAudience, importAppPrivateKey, signAppAssertion, } from './assertion.js';
3
3
  /** Refresh this long before expiry, so no call goes out on a dying token. */
4
4
  const REFRESH_MARGIN_MS = 60_000;
5
5
  /**
@@ -8,6 +8,16 @@ const REFRESH_MARGIN_MS = 60_000;
8
8
  * budgeted 15 s could block for 45 while a stalled mint ran to its own deadline.
9
9
  */
10
10
  const EXCHANGE_TIMEOUT_MS = 15_000;
11
+ /**
12
+ * Longest hold-off this credential takes from a 429's `Retry-After`.
13
+ *
14
+ * The exchange's budget is measured over a trailing minute, so a longer wait is
15
+ * a mistyped limiter or an intermediary's own throttle — and obeying it
16
+ * literally would keep an app from minting for as long as the hint says, on a
17
+ * route whose bucket has certainly freed up by then. The caller still sees the
18
+ * server's uncapped number on `error.retryAfterSeconds`.
19
+ */
20
+ const MAX_RATE_LIMIT_HOLD_MS = 60_000;
11
21
  export function createAppCredential(app, installationId, vendureUrl, fetchImpl) {
12
22
  let token = '';
13
23
  let expiresAt = 0;
@@ -17,7 +27,33 @@ export function createAppCredential(app, installationId, vendureUrl, fetchImpl)
17
27
  /** The dead bearer the in-flight mint is REPLACING, for `refresh()`. */
18
28
  let refreshingFrom = null;
19
29
  let key = null;
30
+ /** Instant the exchange's own 429 said its budget frees up; 0 ⇒ not limited. */
31
+ let rateLimitedUntil = 0;
32
+ /** The refusal a caller gets while the exchange's stated window has not passed. */
33
+ function rateLimited(seconds) {
34
+ return new RateLimitedError({
35
+ code: 'RATE_LIMITED',
36
+ message: `The app token exchange refused the last attempt for this installation and asked to be ` +
37
+ `called again in ${String(seconds)}s. No request was sent.`,
38
+ status: 429,
39
+ service: 'vendure',
40
+ retryAfterSeconds: seconds,
41
+ });
42
+ }
20
43
  async function mint() {
44
+ // HONOUR THE 429 THE EXCHANGE ALREADY ANSWERED, without spending a round
45
+ // trip to be told the same thing. `Retry-After` is the server naming the
46
+ // instant its bucket frees up, and a client that keeps calling through it
47
+ // is the traffic the budget exists to stop. The refusal carries the
48
+ // REMAINING wait, so the caller's own backoff — and the transport's retry
49
+ // loop, which reads `retryAfterSeconds` — paces on the time actually left.
50
+ //
51
+ // Only MINTING is held off. `current()` answers from the cached bearer
52
+ // above, so an app with a live token is untouched by a window it will never
53
+ // reach.
54
+ const holdMs = rateLimitedUntil - Date.now();
55
+ if (holdMs > 0)
56
+ throw rateLimited(Math.ceil(holdMs / 1000));
21
57
  // The generation this mint belongs to. An `invalidate()` that lands while
22
58
  // this is in flight bumps it, and the result is then returned to the caller
23
59
  // but NOT cached — otherwise the token the server has just killed would be
@@ -30,6 +66,10 @@ export function createAppCredential(app, installationId, vendureUrl, fetchImpl)
30
66
  appId: app.appId,
31
67
  installationId,
32
68
  kid: app.kid,
69
+ // The SAME `vendureUrl` the exchange URL is built from, reduced to its
70
+ // origin — so the claim and the endpoint can never name two different
71
+ // deployments, whatever shape the caller configured.
72
+ audience: assertionAudience(vendureUrl),
33
73
  });
34
74
  // Trailing slash stripped, as every other URL in the client is: a
35
75
  // `vendureUrl` of `https://api.example.test/` works for every GraphQL call
@@ -65,8 +105,19 @@ export function createAppCredential(app, installationId, vendureUrl, fetchImpl)
65
105
  }
66
106
  if (!response.ok) {
67
107
  // problem+json first, legacy second — the exchange answers the same shape
68
- // every other refusal on this platform does.
69
- throw await errorFromResponse(response, 'vendure');
108
+ // every other refusal on this platform does. `errorFromResponse` folds the
109
+ // `Retry-After` header into `retryAfterSeconds` for a 429 whose body left
110
+ // it out, so this reads one field either way.
111
+ const error = await errorFromResponse(response, 'vendure');
112
+ if (error.status === 429) {
113
+ const seconds = typeof error.retryAfterSeconds === 'number' && Number.isFinite(error.retryAfterSeconds)
114
+ ? Math.max(0, error.retryAfterSeconds)
115
+ : 0;
116
+ // A 429 with no usable hint sets no hold: guessing a window would be
117
+ // this client inventing a throttle the server never asked for.
118
+ rateLimitedUntil = Date.now() + Math.min(seconds * 1_000, MAX_RATE_LIMIT_HOLD_MS);
119
+ }
120
+ throw error;
70
121
  }
71
122
  // Read inside the same guard as the request: an abort that fires while the
72
123
  // body is still streaming would otherwise escape as a raw `AbortError`,
@@ -54,6 +54,17 @@ export interface AppInstallation {
54
54
  }
55
55
  /** What an approver agrees to. Every field omitted means "keep what was granted". */
56
56
  export interface ApproveConsentInput {
57
+ /**
58
+ * The `pendingConsentReasons` that were on screen when the approver read
59
+ * them.
60
+ *
61
+ * Optional, and worth sending from any surface a human reads: the engine
62
+ * refuses the approval when the app has changed since, so a re-registration
63
+ * between the screen and the button cannot have an escalation ratified by
64
+ * somebody who was never shown it. Omit it and the approval applies to
65
+ * whatever is pending at the moment it lands.
66
+ */
67
+ acknowledgedConsentReasons?: string[];
57
68
  grantedPermissions?: string[];
58
69
  grantedFieldOwnership?: string[];
59
70
  /**
package/dist/client.d.ts CHANGED
@@ -1,19 +1,29 @@
1
1
  import { type DeprecationNotice, type RateLimitInfo, type RetryPolicy, type TenantContext, type TenantScope } from '@sixtyninecommerce/core';
2
2
  import { type ApiTokensModule } from './api-tokens/index.js';
3
+ import { type AppInstallationConfigModule } from './apps/config.js';
3
4
  import { type AppCredentialConfig } from './apps/credential.js';
4
5
  import { type AppsModule } from './apps/index.js';
5
6
  import { type AuditModule } from './audit/index.js';
6
7
  import { type AuthModule } from './auth/index.js';
7
8
  import { type FormsModule } from './content/forms.js';
9
+ import { type CustomObjectsModule } from './custom-objects/index.js';
10
+ import { type CustomersModule } from './customers/index.js';
11
+ import { type DocumentsModule } from './documents/index.js';
8
12
  import { type HeadsModule } from './heads/index.js';
13
+ import { type JobsModule } from './jobs/index.js';
9
14
  import { type LocationsModule } from './locations/index.js';
10
15
  import { type MetaModule } from './meta/index.js';
16
+ import { type MetafieldsModule } from './metafields/index.js';
17
+ import { type OrdersModule } from './orders/index.js';
18
+ import { type PaymentsModule } from './payments/index.js';
11
19
  import { type PlatformAppsModule } from './platform/apps.js';
12
20
  import { type PlatformModule } from './platform/provision-store.js';
13
21
  import { type RedirectsModule } from './seo/redirects.js';
22
+ import { type TaxModule } from './tax/index.js';
14
23
  import { type CapabilitiesModule } from './tenant/capabilities.js';
15
24
  import { type ChannelsModule } from './tenant/channels.js';
16
25
  import { type RetentionModule } from './tenant/retention.js';
26
+ import { type UploadsModule } from './uploads/index.js';
17
27
  export interface AdminClientConfig {
18
28
  urls: {
19
29
  vendureUrl: string;
@@ -60,21 +70,64 @@ export interface AdminClient {
60
70
  auth: AuthModule;
61
71
  apiTokens: ApiTokensModule;
62
72
  /** Installing and managing apps on the ACTIVE store. Needs `InstallApps`. */
63
- apps: AppsModule;
73
+ apps: AppsModule & {
74
+ /** One installation's own surfaces. `config` is its settings form (spec §8.13). */
75
+ installation: {
76
+ config: AppInstallationConfigModule;
77
+ };
78
+ };
64
79
  audit: AuditModule;
65
80
  channels: ChannelsModule;
66
81
  content: {
67
82
  forms: FormsModule;
68
83
  };
84
+ /**
85
+ * Invoices, credit notes and packing slips: issuing them, the numbered facts
86
+ * they were built from, and the store's own extension blocks (spec §8.5).
87
+ */
88
+ documents: DocumentsModule;
89
+ /**
90
+ * Customers — the commerce master record, plus their groups and addresses.
91
+ * `list`/`get` need `ReadCustomer`, the writes `UpdateCustomer`, and `erase`
92
+ * `DeleteCustomer`. `create` is CHANNEL-SCOPED: core `createCustomer` shares
93
+ * a customer across channels by email and is not reachable through this SDK.
94
+ */
95
+ customers: CustomersModule;
69
96
  /** The store's head registry: every client surface it serves (spec §6.7). */
70
97
  heads: HeadsModule;
98
+ /** Every long-running operation the platform runs, under one resource (spec §6.11). */
99
+ jobs: JobsModule;
71
100
  /** Stock-location profiles (spec §8.7); `TenantContext.location` names one by id. */
72
101
  locations: LocationsModule;
73
102
  /** Labels, transitions, permission and plugin catalogs, settings schemas. */
74
103
  meta: MetaModule;
104
+ /**
105
+ * An app's own private state, keyed `(container, key)` (spec §8.13). Only an
106
+ * installation token reaches it.
107
+ */
108
+ customObjects: CustomObjectsModule;
109
+ /** App and platform custom data on core entities (spec §8.13, §8.4). */
110
+ metafields: MetafieldsModule;
111
+ /**
112
+ * Surcharges on a placed order (spec §8.6 for `taxClass`), and the deposit
113
+ * flow: park, read the payment position, settle (spec §8.2, §15.5).
114
+ */
115
+ orders: OrdersModule;
116
+ /**
117
+ * Payment-provider accounts, terminals and readiness (spec §8.9), and money
118
+ * recorded against an order with what it stands at (spec §8.2).
119
+ */
120
+ payments: PaymentsModule;
75
121
  seo: {
76
122
  redirects: RedirectsModule;
77
123
  };
124
+ /** The store's own tax classes and their per-country mappings (spec §8.6). */
125
+ tax: TaxModule;
126
+ /**
127
+ * Signed direct-to-storage uploads (spec §6.11): reserve a place, PUT the
128
+ * bytes straight to the object store, then hand the id to a consuming call.
129
+ */
130
+ uploads: UploadsModule;
78
131
  /** The calling tenant's own settings. Always the ACTIVE channel — no channel argument. */
79
132
  tenant: {
80
133
  capabilities: CapabilitiesModule;
package/dist/client.js CHANGED
@@ -1,19 +1,29 @@
1
1
  import { createChannelResolver, createTransport, mergeTenant, UNSCOPED_TENANT, ValidationError, } from '@sixtyninecommerce/core';
2
2
  import { createApiTokensModule } from './api-tokens/index.js';
3
+ import { createAppConfigModule } from './apps/config.js';
3
4
  import { createAppCredential, } from './apps/credential.js';
4
5
  import { createAppsModule } from './apps/index.js';
5
6
  import { createAuditModule } from './audit/index.js';
6
7
  import { createAuthModule } from './auth/index.js';
7
8
  import { createFormsModule } from './content/forms.js';
9
+ import { createCustomObjectsModule } from './custom-objects/index.js';
10
+ import { createCustomersModule } from './customers/index.js';
11
+ import { createDocumentsModule } from './documents/index.js';
8
12
  import { createHeadsModule } from './heads/index.js';
13
+ import { createJobsModule } from './jobs/index.js';
9
14
  import { createLocationsModule } from './locations/index.js';
10
15
  import { createMetaModule } from './meta/index.js';
16
+ import { createMetafieldsModule } from './metafields/index.js';
17
+ import { createOrdersModule } from './orders/index.js';
18
+ import { createPaymentsModule } from './payments/index.js';
11
19
  import { createPlatformAppsModule } from './platform/apps.js';
12
20
  import { createPlatformModule } from './platform/provision-store.js';
13
21
  import { createRedirectsModule } from './seo/redirects.js';
22
+ import { createTaxModule } from './tax/index.js';
14
23
  import { createCapabilitiesModule } from './tenant/capabilities.js';
15
24
  import { createChannelsModule } from './tenant/channels.js';
16
25
  import { createRetentionModule } from './tenant/retention.js';
26
+ import { createUploadsModule } from './uploads/index.js';
17
27
  /**
18
28
  * An admin credential in a browser is a credential the whole internet can read
19
29
  * (spec §18 amendment 3, §15.21). A SESSION bearer is fine there — that is what
@@ -139,6 +149,17 @@ function assertSessionIsWritable(session) {
139
149
  service: 'vendure',
140
150
  });
141
151
  }
152
+ /**
153
+ * `apps.installation` is BOTH a call and a namespace: `installation(id)` reads
154
+ * one, and `installation.config` is that installation's settings form. The two
155
+ * belong to the same noun, so they share the path rather than growing a second
156
+ * one (`apps.config`) that a reader would have to learn separately.
157
+ */
158
+ function buildAppsModule(transport) {
159
+ const apps = createAppsModule(transport);
160
+ const installation = Object.assign((id, options) => apps.installation(id, options), { config: createAppConfigModule(transport) });
161
+ return { ...apps, installation };
162
+ }
142
163
  export function createAdminClient(config) {
143
164
  assertNotInBrowser(config.auth);
144
165
  const resolver = createChannelResolver({
@@ -324,14 +345,26 @@ function buildClient(config, overrides, resolver, shared, credential) {
324
345
  },
325
346
  }),
326
347
  apiTokens: createApiTokensModule(transport),
327
- apps: createAppsModule(transport),
348
+ apps: buildAppsModule(transport),
328
349
  audit: createAuditModule(transport),
329
350
  channels: createChannelsModule(resolver),
330
351
  content: { forms: createFormsModule(transport) },
331
352
  heads: createHeadsModule(transport),
353
+ jobs: createJobsModule(transport),
332
354
  locations: createLocationsModule(transport),
333
355
  meta: createMetaModule(transport),
356
+ customObjects: createCustomObjectsModule(transport),
357
+ metafields: createMetafieldsModule(transport),
358
+ orders: createOrdersModule(transport),
359
+ payments: createPaymentsModule(transport),
360
+ documents: createDocumentsModule(transport),
361
+ customers: createCustomersModule(transport),
334
362
  seo: { redirects: createRedirectsModule(transport) },
363
+ tax: createTaxModule(transport),
364
+ // The PUT goes to the object store, not to this platform, so it needs the
365
+ // caller's own fetch rather than the transport — which would attach the
366
+ // tenant headers and the bearer to a request that must carry neither.
367
+ uploads: createUploadsModule(transport, config.fetch ?? globalThis.fetch),
335
368
  tenant: {
336
369
  capabilities: createCapabilitiesModule(transport),
337
370
  retention: createRetentionModule(transport),
@@ -0,0 +1,66 @@
1
+ import { type Page, type Transport } from '@sixtyninecommerce/core';
2
+ import type { Call, ListOptions } from '../types.js';
3
+ /**
4
+ * A document an app keeps for itself (spec §8.13).
5
+ *
6
+ * The platform stores and returns the value and interprets none of it. The one
7
+ * exception is the reserved `$config` container, which holds the merchant's
8
+ * settings and is reached through `apps.installation.config` instead — these
9
+ * methods refuse it.
10
+ */
11
+ export interface CustomObject {
12
+ container: string;
13
+ key: string;
14
+ /** Whatever JSON was written. */
15
+ value: unknown;
16
+ /** The stored value's size in bytes, which is what the quota counts. */
17
+ sizeBytes: number;
18
+ createdAt: string;
19
+ updatedAt: string;
20
+ }
21
+ /**
22
+ * An app's own state, addressed by `(container, key)`.
23
+ *
24
+ * **Only an installation token reaches these.** They are the app's private
25
+ * bookkeeping: a human session — even a SuperAdmin's — is refused with
26
+ * `CUSTOM_OBJECT_FORBIDDEN`, and one installation never sees another's rows,
27
+ * on the same channel or on any other. That is what makes it safe for an app to
28
+ * keep an API cursor or a field map here.
29
+ *
30
+ * **Two quotas, per installation per store.** A single document may be 128 KiB,
31
+ * and an installation may hold 10,000 live objects
32
+ * (`CUSTOM_OBJECT_TOO_LARGE`, `CUSTOM_OBJECT_LIMIT_REACHED`). The row quota is
33
+ * checked only when a NEW key is written, so a full installation can still
34
+ * shrink a value it already has.
35
+ *
36
+ * **Names are capped.** A container is 1-64 characters, starts with a lowercase
37
+ * letter or digit and uses only lowercase letters, digits, `-` and `_`; a key is
38
+ * 1-200 characters and carries no control characters. `$config` is reserved for
39
+ * the settings form and is reached through `apps.installation.config`.
40
+ *
41
+ * **The value must be JSON that survives a round trip.** `undefined`, a
42
+ * function and a non-finite number are refused at any depth
43
+ * (`CUSTOM_OBJECT_INVALID`) rather than stored as `null` and read back as
44
+ * something nobody wrote.
45
+ */
46
+ export interface CustomObjectsModule {
47
+ /** One container's objects, newest first. */
48
+ list(container: string, options?: ListOptions): Promise<Page<CustomObject>>;
49
+ /** One object. Unknown `(container, key)` is `CUSTOM_OBJECT_NOT_FOUND`. */
50
+ get(container: string, key: string, options?: Call): Promise<CustomObject>;
51
+ /**
52
+ * Create or replace one object. Last write wins.
53
+ *
54
+ * An `idempotencyKey` is forwarded if you pass one, but the route does NOT
55
+ * honour it: the store namespaces keys by channel while these rows are per
56
+ * installation, so a shared namespace would let one app's key collide with
57
+ * another's. A PUT that replaces a value by key needs no key of its own —
58
+ * repeating it is the same as doing it once.
59
+ */
60
+ set(container: string, key: string, value: unknown, options?: Call & {
61
+ idempotencyKey?: string;
62
+ }): Promise<CustomObject>;
63
+ /** Delete one object. Deleting what is not there is `CUSTOM_OBJECT_NOT_FOUND`. */
64
+ delete(container: string, key: string, options?: Call): Promise<void>;
65
+ }
66
+ export declare function createCustomObjectsModule(transport: Transport): CustomObjectsModule;
@@ -0,0 +1,48 @@
1
+ import { createPage, PLATFORM_PASSTHROUGH_KEYS, readRestPage, } from '@sixtyninecommerce/core';
2
+ /**
3
+ * `value` holds the APP's own JSON, so its inner keys must survive the
4
+ * transport's case conversion untouched.
5
+ *
6
+ * Without this an app that writes `{ order_ref: 1 }` reads back
7
+ * `{ orderRef: 1 }` — and the round trip hides it, because the read path
8
+ * converts the mangled key into something that looks right while the stored row
9
+ * is wrong. This is the same trap `PLATFORM_PASSTHROUGH_KEYS` exists for; the
10
+ * key is added here rather than to that shared set because `value` is a common
11
+ * enough name that preserving it platform-wide would change modules that mean
12
+ * something else by it.
13
+ */
14
+ const PRESERVE = new Set([...PLATFORM_PASSTHROUGH_KEYS, 'value']);
15
+ const segment = (value) => encodeURIComponent(value);
16
+ export function createCustomObjectsModule(transport) {
17
+ async function list(container, options) {
18
+ const body = await transport.rest('plugins', {
19
+ method: 'GET',
20
+ path: `/custom-objects/${segment(container)}`,
21
+ query: { after: options?.after, limit: options?.limit },
22
+ }, { ...options, preserveKeys: PRESERVE });
23
+ const { items, meta } = readRestPage(body);
24
+ return createPage(items, meta, (cursor) => list(container, { ...options, after: cursor }));
25
+ }
26
+ return {
27
+ list,
28
+ async get(container, key, options) {
29
+ return await transport.rest('plugins', { method: 'GET', path: `/custom-objects/${segment(container)}/${segment(key)}` }, { ...options, preserveKeys: PRESERVE });
30
+ },
31
+ async set(container, key, value, options) {
32
+ return await transport.rest('plugins', {
33
+ method: 'PUT',
34
+ path: `/custom-objects/${segment(container)}/${segment(key)}`,
35
+ // Wrapped rather than sent bare so the body can gain a field later;
36
+ // a bare JSON value has nowhere to put one.
37
+ body: { value },
38
+ }, { ...options, preserveKeys: PRESERVE });
39
+ },
40
+ async delete(container, key, options) {
41
+ await transport.rest('plugins', {
42
+ method: 'DELETE',
43
+ path: `/custom-objects/${segment(container)}/${segment(key)}`,
44
+ expect: 'void',
45
+ }, options);
46
+ },
47
+ };
48
+ }
@@ -0,0 +1,225 @@
1
+ import { type Page, type Transport } from '@sixtyninecommerce/core';
2
+ import type { Call, ListOptions } from '../types.js';
3
+ /**
4
+ * Customers — the commerce master record (headless spec §6.5).
5
+ *
6
+ * Commerce owns customer identity and addresses; a vertical stores a
7
+ * `platformCustomerId` and asks here. The two things worth knowing before
8
+ * reading further are both traps this module exists to close:
9
+ *
10
+ * 1. **`create` wraps `createChannelCustomer`, never core `createCustomer`.**
11
+ * Vendure's own mutation silently SHARES a customer across channels when the
12
+ * email already exists anywhere on the platform (ADR-016): it finds that
13
+ * customer and pushes the new channel onto them. On a multi-tenant platform
14
+ * that is a cross-tenant data leak wearing the clothes of a convenience.
15
+ * There is no way to reach the leaking path through this SDK.
16
+ * 2. **`erase` is a scrub, not a delete.** It removes the person and keeps the
17
+ * record, the orders and the documents. It is the only destructive customer
18
+ * operation the platform has.
19
+ */
20
+ export declare const CUSTOMER_FIELDS = "\n id\n createdAt\n updatedAt\n title\n firstName\n lastName\n emailAddress\n phoneNumber\n groups {\n id\n name\n }\n";
21
+ export declare const CUSTOMER_ADDRESS_FIELDS = "\n id\n fullName\n company\n streetLine1\n streetLine2\n city\n province\n postalCode\n country {\n code\n name\n }\n phoneNumber\n defaultShippingAddress\n defaultBillingAddress\n";
22
+ export declare const LIST_QUERY = "\n query Customers($options: CustomerListOptions) {\n customers(options: $options) {\n items { \n id\n createdAt\n updatedAt\n title\n firstName\n lastName\n emailAddress\n phoneNumber\n groups {\n id\n name\n }\n }\n totalItems\n }\n }\n";
23
+ export declare const GET_QUERY = "\n query CustomerById($id: ID!) {\n customer(id: $id) { \n id\n createdAt\n updatedAt\n title\n firstName\n lastName\n emailAddress\n phoneNumber\n groups {\n id\n name\n }\n }\n }\n";
24
+ export declare const ADDRESSES_QUERY = "\n query CustomerAddresses($id: ID!) {\n customer(id: $id) {\n id\n addresses { \n id\n fullName\n company\n streetLine1\n streetLine2\n city\n province\n postalCode\n country {\n code\n name\n }\n phoneNumber\n defaultShippingAddress\n defaultBillingAddress\n }\n }\n }\n";
25
+ /**
26
+ * The CHANNEL-SCOPED creation mutation. See the module docblock — core
27
+ * `createCustomer` is not an option, and this SDK exposes no path to it.
28
+ */
29
+ export declare const CREATE_MUTATION = "\n mutation CreateChannelCustomer($input: CreateCustomerInput!, $password: String) {\n createChannelCustomer(input: $input, password: $password) {\n id\n emailAddress\n createdNew\n }\n }\n";
30
+ export declare const UPDATE_MUTATION = "\n mutation UpdateCustomer($input: UpdateCustomerInput!) {\n updateCustomer(input: $input) {\n ... on Customer { \n id\n createdAt\n updatedAt\n title\n firstName\n lastName\n emailAddress\n phoneNumber\n groups {\n id\n name\n }\n }\n ... on ErrorResult { errorCode message }\n }\n }\n";
31
+ export declare const INVITE_MUTATION = "\n mutation InviteCustomer($customerId: ID!, $mode: CustomerInviteMode) {\n inviteCustomer(customerId: $customerId, mode: $mode) {\n customerId\n emailAddress\n mode\n sentAt\n }\n }\n";
32
+ export declare const RESEND_INVITE_MUTATION = "\n mutation ResendCustomerInvitation($customerId: ID!, $mode: CustomerInviteMode) {\n resendCustomerInvitation(customerId: $customerId, mode: $mode) {\n customerId\n emailAddress\n mode\n sentAt\n }\n }\n";
33
+ export declare const ERASE_MUTATION = "\n mutation EraseCustomer(\n $customerId: ID!\n $reason: String!\n $force: Boolean\n $idempotencyKey: String\n ) {\n eraseCustomer(\n customerId: $customerId\n reason: $reason\n force: $force\n idempotencyKey: $idempotencyKey\n ) {\n customerId\n erasedAt\n ordersRetained\n documentsRetained\n forced\n reason\n }\n }\n";
34
+ export declare const ERASURE_QUERY = "\n query CustomerErasure($customerId: ID!) {\n customerErasure(customerId: $customerId) {\n customerId\n erasedAt\n ordersRetained\n documentsRetained\n forced\n reason\n }\n }\n";
35
+ export declare const ADD_TO_GROUP_MUTATION = "\n mutation AddCustomersToGroup($customerGroupId: ID!, $customerIds: [ID!]!) {\n addCustomersToGroup(customerGroupId: $customerGroupId, customerIds: $customerIds) {\n id\n name\n }\n }\n";
36
+ export declare const REMOVE_FROM_GROUP_MUTATION = "\n mutation RemoveCustomersFromGroup($customerGroupId: ID!, $customerIds: [ID!]!) {\n removeCustomersFromGroup(customerGroupId: $customerGroupId, customerIds: $customerIds) {\n id\n name\n }\n }\n";
37
+ export declare const CREATE_ADDRESS_MUTATION = "\n mutation CreateCustomerAddress($customerId: ID!, $input: CreateAddressInput!) {\n createCustomerAddress(customerId: $customerId, input: $input) {\n \n id\n fullName\n company\n streetLine1\n streetLine2\n city\n province\n postalCode\n country {\n code\n name\n }\n phoneNumber\n defaultShippingAddress\n defaultBillingAddress\n\n }\n }\n";
38
+ export declare const UPDATE_ADDRESS_MUTATION = "\n mutation UpdateCustomerAddress($input: UpdateAddressInput!) {\n updateCustomerAddress(input: $input) { \n id\n fullName\n company\n streetLine1\n streetLine2\n city\n province\n postalCode\n country {\n code\n name\n }\n phoneNumber\n defaultShippingAddress\n defaultBillingAddress\n }\n }\n";
39
+ export declare const DELETE_ADDRESS_MUTATION = "\n mutation DeleteCustomerAddress($id: ID!) {\n deleteCustomerAddress(id: $id) { success }\n }\n";
40
+ export interface CustomerGroupRef {
41
+ id: string;
42
+ name: string;
43
+ }
44
+ export interface Customer {
45
+ id: string;
46
+ createdAt: string;
47
+ updatedAt: string;
48
+ title: string | null;
49
+ firstName: string;
50
+ lastName: string;
51
+ emailAddress: string;
52
+ phoneNumber: string | null;
53
+ groups: CustomerGroupRef[];
54
+ /** Forward-compat: fields a later release adds arrive camelised and untouched. */
55
+ [extra: string]: unknown;
56
+ }
57
+ export interface CustomerAddress {
58
+ id: string;
59
+ fullName: string | null;
60
+ company: string | null;
61
+ streetLine1: string;
62
+ streetLine2: string | null;
63
+ city: string | null;
64
+ province: string | null;
65
+ postalCode: string | null;
66
+ country: {
67
+ code: string;
68
+ name: string;
69
+ } | null;
70
+ phoneNumber: string | null;
71
+ defaultShippingAddress: boolean | null;
72
+ defaultBillingAddress: boolean | null;
73
+ [extra: string]: unknown;
74
+ }
75
+ export interface CreateCustomerInput {
76
+ emailAddress: string;
77
+ firstName: string;
78
+ lastName: string;
79
+ title?: string;
80
+ phoneNumber?: string;
81
+ customFields?: Record<string, unknown>;
82
+ }
83
+ export interface UpdateCustomerInput {
84
+ title?: string;
85
+ firstName?: string;
86
+ lastName?: string;
87
+ phoneNumber?: string;
88
+ emailAddress?: string;
89
+ customFields?: Record<string, unknown>;
90
+ }
91
+ /** What `createChannelCustomer` answers. */
92
+ export interface CreatedCustomer {
93
+ id: string;
94
+ emailAddress: string;
95
+ /**
96
+ * `false` when a customer with this email already existed ON THIS CHANNEL and
97
+ * was returned instead. It is never another channel's customer — that is the
98
+ * whole point of the channel-scoped mutation.
99
+ */
100
+ createdNew: boolean;
101
+ }
102
+ export type CustomerInviteMode = 'PASSWORD_SETUP' | 'MAGIC_LINK';
103
+ export interface CustomerInvitation {
104
+ customerId: string;
105
+ emailAddress: string;
106
+ mode: CustomerInviteMode;
107
+ sentAt: string;
108
+ }
109
+ /** The record of an erasure. See {@link CustomersModule.erase}. */
110
+ export interface CustomerErasureRecord {
111
+ customerId: string;
112
+ erasedAt: string;
113
+ /** Placed orders left standing — an order is a document fact. */
114
+ ordersRetained: number;
115
+ /**
116
+ * ORDER DOCUMENTS left standing: invoices and credit notes issued against
117
+ * this customer's orders on this channel.
118
+ *
119
+ * Accounts-receivable documents (`ar_invoice`, `ar_credit_note`) are NOT
120
+ * counted — a consolidated AR invoice spans several orders and is not
121
+ * reachable through the order join — so a B2B tenant's full document set is
122
+ * larger than this number.
123
+ */
124
+ documentsRetained: number;
125
+ /** A SuperAdmin overrode the open-unpaid-order refusal. */
126
+ forced: boolean;
127
+ reason: string;
128
+ }
129
+ export interface EraseCustomerOptions extends Call {
130
+ /** Recorded on the erasure record. Required — it is the evidence it was honoured. */
131
+ reason: string;
132
+ /**
133
+ * Erase despite an open unpaid order. SuperAdmin only, refused with
134
+ * `CUSTOMER_ERASURE_FORCE_FORBIDDEN` otherwise, and recorded on the record.
135
+ */
136
+ force?: boolean;
137
+ /** Your own request id, recorded for provenance. */
138
+ idempotencyKey?: string;
139
+ }
140
+ export interface CustomerAddressesModule {
141
+ list(customerId: string, options?: Call): Promise<CustomerAddress[]>;
142
+ create(customerId: string, input: Record<string, unknown>, options?: Call): Promise<CustomerAddress>;
143
+ update(input: {
144
+ id: string;
145
+ } & Record<string, unknown>, options?: Call): Promise<CustomerAddress>;
146
+ delete(id: string, options?: Call): Promise<void>;
147
+ }
148
+ export interface CustomerGroupsModule {
149
+ /** Add customers to a group. Emits one `customer.group_changed` per customer. */
150
+ add(groupId: string, customerIds: string[], options?: Call): Promise<CustomerGroupRef>;
151
+ remove(groupId: string, customerIds: string[], options?: Call): Promise<CustomerGroupRef>;
152
+ }
153
+ export interface CustomersModule {
154
+ list(options?: ListOptions): Promise<Page<Customer>>;
155
+ get(id: string, options?: Call): Promise<Customer | null>;
156
+ /**
157
+ * Create a customer ON THIS CHANNEL. Never creates credentials — `invite`
158
+ * does that.
159
+ */
160
+ create(input: CreateCustomerInput, options?: Call & {
161
+ password?: string;
162
+ }): Promise<CreatedCustomer>;
163
+ update(id: string, input: UpdateCustomerInput, options?: Call): Promise<Customer>;
164
+ /**
165
+ * Mail the customer a way in: a set-password link, or a magic-link sign-in.
166
+ *
167
+ * Not the storefront's forgot-password screen, which mails a password RESET
168
+ * — something a customer who has never had an account did not ask for.
169
+ */
170
+ invite(id: string, options?: Call & {
171
+ mode?: CustomerInviteMode;
172
+ resend?: boolean;
173
+ }): Promise<CustomerInvitation>;
174
+ /**
175
+ * GDPR erasure: scrub the person, keep the record.
176
+ *
177
+ * The customer row, their orders and their invoices remain — an invoice that
178
+ * cannot be reproduced is a different problem from a privacy one. Emits
179
+ * `customer.deleted`.
180
+ *
181
+ * What "the person" covers is enumerated BY COLUMN on the platform rather
182
+ * than remembered: every column of the commerce database classified as
183
+ * personal data is scrubbed or tombstoned, retained columns and their reasons
184
+ * are listed with it, and the plugins-side event log is redacted too. The
185
+ * classification is regenerated from the live schema and a build
186
+ * fails on any column it does not answer for — including columns on tables
187
+ * that name no customer at all, which is how an invoice email log kept an
188
+ * erased person's address.
189
+ *
190
+ * Refused while a retention hold is active (`RETENTION_HOLD_ACTIVE`), while
191
+ * the customer is party to an open unpaid order (`CUSTOMER_HAS_OPEN_ORDERS`;
192
+ * a SuperAdmin may pass `force`), while they hold a subscription that has not
193
+ * ended (`CUSTOMER_HAS_ACTIVE_SUBSCRIPTIONS`), when their login is shared with
194
+ * another store on the platform (`CUSTOMER_SHARED_ACROSS_STORES`), and when
195
+ * the tenant context names the platform root channel rather than the
196
+ * customer's own (`CUSTOMER_ERASURE_CHANNEL_INVALID`).
197
+ *
198
+ * `force` overrides the OPEN-ORDER refusal and nothing else. A live payment
199
+ * mandate will charge this person again next cycle, and a shared login belongs
200
+ * to a merchant who asked for nothing — neither is a SuperAdmin's to waive.
201
+ *
202
+ * Idempotent: erasing an already-erased customer returns the original record.
203
+ *
204
+ * Platform events emitted BEFORE the erasure lose the name and email too. That
205
+ * log lives in another service's database, which the erasure cannot reach, so
206
+ * the redaction is triggered there by the arrival of this erasure's own
207
+ * `customer.deleted` event: ingesting it queues the redaction in the same
208
+ * transaction, and a worker applies it afterwards, so this call RETURNS
209
+ * BEFORE the log has been rewritten. Two one-minute ticks sit in between (the
210
+ * relay, then the drain), so it is up to about two minutes. A periodic
211
+ * reconcile covers deletions ingested before that release shipped, and a
212
+ * redaction that keeps failing is parked and reported in the plugins log —
213
+ * nothing on the returned record reflects either.
214
+ *
215
+ * That event keeps its ids and its `external_refs`: it is the one moment an
216
+ * ESP, ERP, CRM or PSP can erase its own copy of the person, and blanking it
217
+ * would destroy the only keys to them.
218
+ */
219
+ erase(id: string, options: EraseCustomerOptions): Promise<CustomerErasureRecord>;
220
+ /** The erasure record, or `null` if this customer has not been erased. */
221
+ erasure(id: string, options?: Call): Promise<CustomerErasureRecord | null>;
222
+ groups: CustomerGroupsModule;
223
+ addresses: CustomerAddressesModule;
224
+ }
225
+ export declare function createCustomersModule(transport: Transport): CustomersModule;