@doany-ai/sdk 0.2.8 → 0.3.0-alpha.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.
@@ -0,0 +1,112 @@
1
+ import type { Address, CreateOptions, Page, PageParams, VersionInput } from "./project.types";
2
+ export type ContactSource = "website_registration" | "purchase" | "booking_form" | "contact_form" | "staff_entry" | "agent" | "import" | "api" | "unknown";
3
+ /**
4
+ * Someone the business knows: a customer, a lead, someone who wrote in.
5
+ *
6
+ * `business_note`, `source`, `app_id`, `merged_into_id` and `merged_at` are
7
+ * shown to the site's admin and service callers only.
8
+ */
9
+ export interface Contact {
10
+ id: string;
11
+ /** Always set. */
12
+ display_name: string;
13
+ given_name: string | null;
14
+ family_name: string | null;
15
+ /** Lower-case. */
16
+ email: string | null;
17
+ /** E.164, e.g. `+14155550123`. */
18
+ phone: string | null;
19
+ address: Address | null;
20
+ timezone: string | null;
21
+ preferred_language: string | null;
22
+ business_note?: string | null;
23
+ source?: ContactSource;
24
+ /** The site it came from; `null` for one the business entered. */
25
+ app_id?: string | null;
26
+ archived_at: string | null;
27
+ merged_into_id?: string | null;
28
+ merged_at?: string | null;
29
+ version: number;
30
+ created_at: string;
31
+ updated_at: string;
32
+ }
33
+ /** An account linked to a contact. */
34
+ export interface ContactUserLink {
35
+ user_id: string;
36
+ email: string | null;
37
+ /** `self`: the account is this person. */
38
+ role: "self" | "admin" | "billing" | "member";
39
+ /** `records`: sees the contact's orders and buys as it. `profile`: its details only. */
40
+ access: "profile" | "records";
41
+ /** The contact this account buys as by default. */
42
+ is_default: boolean;
43
+ granted_via: "registration" | "email_match" | "staff" | "checkout" | "invite" | "claim";
44
+ status: "active" | "revoked";
45
+ }
46
+ /** A contact with the accounts linked to it (the latter for admin and service callers). */
47
+ export interface ContactDetail extends Contact {
48
+ users?: ContactUserLink[];
49
+ }
50
+ export interface ContactListParams extends PageParams {
51
+ /** Part of the name, email or phone. */
52
+ q?: string;
53
+ /** `active` (default), `archived` or `all`. */
54
+ status?: "active" | "archived" | "all";
55
+ source?: ContactSource;
56
+ /** Contacts that came from this site. */
57
+ app_id?: string;
58
+ created_from?: string;
59
+ created_to?: string;
60
+ }
61
+ interface ContactFields {
62
+ given_name?: string | null;
63
+ family_name?: string | null;
64
+ email?: string | null;
65
+ phone?: string | null;
66
+ address?: Partial<Address> | null;
67
+ timezone?: string | null;
68
+ preferred_language?: string | null;
69
+ business_note?: string | null;
70
+ }
71
+ export interface CreateContactParams extends ContactFields {
72
+ /** Left out: the name, else the email, else the phone. */
73
+ display_name?: string | null;
74
+ /** `staff_entry` (default), `agent`, `import`, `api`. */
75
+ source?: "staff_entry" | "agent" | "import" | "api";
76
+ }
77
+ export interface UpdateContactParams extends ContactFields, VersionInput {
78
+ display_name?: string;
79
+ }
80
+ /**
81
+ * The people the business knows.
82
+ *
83
+ * A signed-in account reads and changes only its own contact (`me`, and
84
+ * `updateMe` — not its email, which follows its sign-in). Everything else
85
+ * takes the site's admin account or a service credential.
86
+ */
87
+ export interface ContactsModule {
88
+ /** Contacts the caller may see; an account sees only its own. Rejects with 401 when nobody is signed in. */
89
+ list(params?: ContactListParams): Promise<Page<Contact>>;
90
+ /** One contact. Rejects with 404 when there is none, or the caller may not see it. */
91
+ get(contactId: string): Promise<ContactDetail>;
92
+ /** The signed-in account's own contact. Rejects with 404 when it has none yet. */
93
+ me(): Promise<ContactDetail>;
94
+ /** A new contact. Rejects with 400 on a malformed email or phone. */
95
+ create(params: CreateContactParams, options?: CreateOptions): Promise<Contact>;
96
+ update(contactId: string, params: UpdateContactParams): Promise<Contact>;
97
+ /** Changes the signed-in account's own contact. */
98
+ updateMe(params: UpdateContactParams): Promise<Contact>;
99
+ archive(contactId: string, params: VersionInput): Promise<Contact>;
100
+ restore(contactId: string, params: VersionInput): Promise<Contact>;
101
+ /**
102
+ * Merges a duplicate into another contact, for good: its account links move
103
+ * to the target and its orders are found under the target from then on.
104
+ * Returns the target.
105
+ */
106
+ merge(contactId: string, params: VersionInput & {
107
+ into_contact_id: string;
108
+ }): Promise<ContactDetail>;
109
+ /** Deletes a contact nothing refers to; otherwise 409 `IN_USE` (archive it instead). */
110
+ delete(contactId: string): Promise<void>;
111
+ }
112
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import { AxiosInstance } from "axios";
2
+ import { EventsModule } from "./events.types";
3
+ import { ProjectScope } from "./project.js";
4
+ /**
5
+ * Creates the events module: `/projects/{project_id}/events`.
6
+ *
7
+ * @internal
8
+ */
9
+ export declare function createEventsModule(axios: AxiosInstance, project: ProjectScope): EventsModule;
@@ -0,0 +1,15 @@
1
+ import { queryOf } from "./project.js";
2
+ /**
3
+ * Creates the events module: `/projects/{project_id}/events`.
4
+ *
5
+ * @internal
6
+ */
7
+ export function createEventsModule(axios, project) {
8
+ return {
9
+ async list(params = {}) {
10
+ return (await axios.get(await project.path("/events"), {
11
+ params: queryOf(params),
12
+ }));
13
+ },
14
+ };
15
+ }
@@ -0,0 +1,42 @@
1
+ import type { Page } from "./project.types";
2
+ export type EventSubjectType = "order" | "payment" | "refund" | "dispute" | "contact";
3
+ /**
4
+ * Something that happened to an order, a payment, a refund, a dispute or a
5
+ * contact — `order.placed`, `payment.succeeded`, `contact.merged`, … — and who
6
+ * did it.
7
+ */
8
+ export interface ProjectEvent {
9
+ id: string;
10
+ event_type: string;
11
+ subject_type: EventSubjectType;
12
+ subject_id: string;
13
+ /** `type`: `system`, `provider`, `founder` (+ `user_id`), `agent`, `customer` (+ `project_user_id`), `guest`. */
14
+ actor: {
15
+ type: string;
16
+ user_id?: string;
17
+ project_user_id?: string;
18
+ };
19
+ data: Record<string, any>;
20
+ occurred_at: string;
21
+ }
22
+ export interface EventListParams {
23
+ /** The order's events, and those of its payments, refunds and disputes. */
24
+ order_id?: string;
25
+ /** The contact's (and merged contacts') events, and those of their orders. */
26
+ contact_id?: string;
27
+ subject_type?: EventSubjectType;
28
+ subject_id?: string;
29
+ /** A full type (`order.placed`) or a prefix ending in `.` (`refund.`). */
30
+ event_type?: string;
31
+ occurred_from?: string;
32
+ occurred_to?: string;
33
+ limit?: number;
34
+ cursor?: string;
35
+ }
36
+ /**
37
+ * The business's timeline. The site's admin and service callers only.
38
+ */
39
+ export interface EventsModule {
40
+ /** Newest first. */
41
+ list(params?: EventListParams): Promise<Page<ProjectEvent>>;
42
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ import { AxiosInstance } from "axios";
2
+ /**
3
+ * An order's access token (`X-Doany-Access-Token`): what lets a guest come back
4
+ * to an order they placed. Kept for the browser session by `orders.create` and
5
+ * sent by every call that accepts it — `orders.get`, `orders.cancel`,
6
+ * `payments.checkout`, `payments.getForOrder`.
7
+ *
8
+ * @internal
9
+ */
10
+ /** cyrb53: a short, stable name for a value, so the value itself (a token, a
11
+ * buyer's details) is not what gets written as a storage key. */
12
+ export declare function nameOf(text: string): string;
13
+ /** Who the backend will take this request to be from: it keeps idempotency
14
+ * keys per signed-in account and per guest, so an unresolved attempt belongs
15
+ * to whoever made it and nobody else may reuse or clear it. */
16
+ export declare function actorOf(axios: AxiosInstance): string;
17
+ export declare function session(): Storage | null;
18
+ export declare function rememberOrderToken(axios: AxiosInstance, appId: string, actor: string, orderId: string, token?: string): void;
19
+ /** The header carrying this order's token: the one given, else the one kept. */
20
+ export declare function orderAccessHeaders(axios: AxiosInstance, appId: string, orderId: string, accessToken?: string): Record<string, string>;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * An order's access token (`X-Doany-Access-Token`): what lets a guest come back
3
+ * to an order they placed. Kept for the browser session by `orders.create` and
4
+ * sent by every call that accepts it — `orders.get`, `orders.cancel`,
5
+ * `payments.checkout`, `payments.getForOrder`.
6
+ *
7
+ * @internal
8
+ */
9
+ /** cyrb53: a short, stable name for a value, so the value itself (a token, a
10
+ * buyer's details) is not what gets written as a storage key. */
11
+ export function nameOf(text) {
12
+ let h1 = 0xdeadbeef;
13
+ let h2 = 0x41c6ce57;
14
+ for (let i = 0; i < text.length; i++) {
15
+ const ch = text.charCodeAt(i);
16
+ h1 = Math.imul(h1 ^ ch, 2654435761);
17
+ h2 = Math.imul(h2 ^ ch, 1597334677);
18
+ }
19
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
20
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
21
+ return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(36);
22
+ }
23
+ /** Who the backend will take this request to be from: it keeps idempotency
24
+ * keys per signed-in account and per guest, so an unresolved attempt belongs
25
+ * to whoever made it and nobody else may reuse or clear it. */
26
+ export function actorOf(axios) {
27
+ var _a, _b, _c;
28
+ const header = (_c = (_b = (_a = axios.defaults) === null || _a === void 0 ? void 0 : _a.headers) === null || _b === void 0 ? void 0 : _b.common) === null || _c === void 0 ? void 0 : _c["Authorization"];
29
+ if (typeof header !== "string" || !header.startsWith("Bearer "))
30
+ return "guest";
31
+ try {
32
+ const payload = header.split(".")[1].replace(/-/g, "+").replace(/_/g, "/");
33
+ const sub = JSON.parse(atob(payload)).sub;
34
+ if (typeof sub === "string" && sub)
35
+ return `user:${sub}`;
36
+ }
37
+ catch (_d) {
38
+ /* not a JWT we can read */
39
+ }
40
+ return `token:${nameOf(header)}`;
41
+ }
42
+ export function session() {
43
+ try {
44
+ return typeof window !== "undefined" ? window.sessionStorage : null;
45
+ }
46
+ catch (_a) {
47
+ return null; // storage can be blocked (private mode, sandboxed iframe)
48
+ }
49
+ }
50
+ // A kept token belongs to whoever placed the order. It is stored under that
51
+ // actor and looked up under the current one — plus `guest`, so an order placed
52
+ // before signing in can still be opened after. What it never does is follow
53
+ // the tab to another account, or stay usable once its account signed out.
54
+ function tokenKey(axios, appId, actor, orderId) {
55
+ var _a, _b;
56
+ return `doany_order_token_${appId}_${nameOf(`${(_b = (_a = axios.defaults) === null || _a === void 0 ? void 0 : _a.baseURL) !== null && _b !== void 0 ? _b : ""}|${actor}`)}_${orderId}`;
57
+ }
58
+ export function rememberOrderToken(axios, appId, actor, orderId, token) {
59
+ var _a;
60
+ if (!token)
61
+ return;
62
+ try {
63
+ (_a = session()) === null || _a === void 0 ? void 0 : _a.setItem(tokenKey(axios, appId, actor, orderId), token);
64
+ }
65
+ catch (_b) {
66
+ /* best effort */
67
+ }
68
+ }
69
+ /** The header carrying this order's token: the one given, else the one kept. */
70
+ export function orderAccessHeaders(axios, appId, orderId, accessToken) {
71
+ var _a, _b;
72
+ let token = accessToken;
73
+ if (!token) {
74
+ try {
75
+ const store = session();
76
+ token =
77
+ (_b = (_a = store === null || store === void 0 ? void 0 : store.getItem(tokenKey(axios, appId, actorOf(axios), orderId))) !== null && _a !== void 0 ? _a : store === null || store === void 0 ? void 0 : store.getItem(tokenKey(axios, appId, "guest", orderId))) !== null && _b !== void 0 ? _b : undefined;
78
+ }
79
+ catch (_c) {
80
+ token = undefined;
81
+ }
82
+ }
83
+ return token ? { "X-Doany-Access-Token": token } : {};
84
+ }
@@ -0,0 +1,25 @@
1
+ import { AxiosInstance } from "axios";
2
+ import { OrdersModule } from "./orders.types";
3
+ import { ProjectScope } from "./project.js";
4
+ /** @internal Tests start from a page that has placed nothing. */
5
+ export declare function resetOrderAttempts(): void;
6
+ /**
7
+ * Creates the orders module for the Doany SDK.
8
+ *
9
+ * Two things are handled here so that page code does not have to:
10
+ *
11
+ * - **Retries do not double-order.** The backend requires an `Idempotency-Key`.
12
+ * Identical calls that overlap are one request (a double click), and a key
13
+ * is kept for an identical order for as long as its outcome is unknown (it
14
+ * failed without an answer), so a retry after a dropped connection lands on
15
+ * the same order — also after a page reload, for 15 minutes. Once the
16
+ * server has answered, the next identical order is a new one.
17
+ * - **A guest can reopen their order.** The order's access token is kept for
18
+ * the browser session and sent with `get` / `cancel` for that order, and
19
+ * with `payments.checkout` / `payments.getForOrder` (order-access.ts).
20
+ *
21
+ * @internal
22
+ */
23
+ export declare function createOrdersModule(axios: AxiosInstance, appId: string, project: ProjectScope, { rememberAttempts, }?: {
24
+ rememberAttempts?: boolean;
25
+ }): OrdersModule;
@@ -0,0 +1,296 @@
1
+ import { actorOf, nameOf, orderAccessHeaders, rememberOrderToken, session } from "./order-access.js";
2
+ import { queryOf, seg } from "./project.js";
3
+ // Shared by every client the page creates: two clients placing the same order
4
+ // at once are still one attempt, and neither may release a key the other is
5
+ // still waiting on. Entries are named by app, actor and request.
6
+ const inFlight = new Map();
7
+ const memory = new Map();
8
+ /** @internal Tests start from a page that has placed nothing. */
9
+ export function resetOrderAttempts() {
10
+ inFlight.clear();
11
+ memory.clear();
12
+ }
13
+ function newKey() {
14
+ const c = globalThis.crypto;
15
+ if (c === null || c === void 0 ? void 0 : c.randomUUID)
16
+ return `order-${c.randomUUID()}`;
17
+ let out = "order-";
18
+ for (let i = 0; i < 32; i++)
19
+ out += Math.floor(Math.random() * 16).toString(16);
20
+ return out;
21
+ }
22
+ /**
23
+ * How long an order whose outcome is unknown keeps its key across page loads.
24
+ * Long enough to cover "the request failed, reload, try again"; short enough
25
+ * that ordering the same things again later is a new order.
26
+ */
27
+ const ATTEMPT_TTL_MS = 15 * 60 * 1000;
28
+ /**
29
+ * The request exactly as it will be sent, in one canonical form: keys sorted,
30
+ * `quantity` filled in with its default, `variant_id` lower-cased.
31
+ *
32
+ * The retry key is looked up by THIS, and this is what goes over the wire, so
33
+ * "same key" always means "the very same request". Anything looser breaks in
34
+ * one of two ways: write the same order with the keys in another order and it
35
+ * would get a second key (a duplicate order), or fold two requests the backend
36
+ * tells apart — a stray space, a blank optional field — into one and the
37
+ * refusal of one would release the key of the other. So whatever is
38
+ * normalised here is normalised in the request itself, never only in the key.
39
+ */
40
+ function canonical(value) {
41
+ if (Array.isArray(value))
42
+ return value.map(canonical);
43
+ if (value && typeof value === "object") {
44
+ const out = {};
45
+ for (const key of Object.keys(value).sort()) {
46
+ const item = value[key];
47
+ if (item !== undefined)
48
+ out[key] = canonical(item);
49
+ }
50
+ return out;
51
+ }
52
+ return value;
53
+ }
54
+ const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
55
+ /** A valid name or email in the one form the backend stores it in, so editing
56
+ * a stray space or a capital between a failed attempt and its retry is still
57
+ * the same order. Anything blank or invalid is left exactly as typed: the
58
+ * backend refuses it, and a refused request must stay its own request. */
59
+ function tidyBuyer(buyer) {
60
+ if (!buyer || typeof buyer !== "object")
61
+ return buyer;
62
+ const out = { ...buyer };
63
+ // `email: null` and no email are the same thing to the backend.
64
+ for (const optional of ["email", "phone"]) {
65
+ if (out[optional] === null)
66
+ delete out[optional];
67
+ }
68
+ if (typeof out.name === "string" && out.name.trim())
69
+ out.name = out.name.trim();
70
+ if (typeof out.email === "string" && EMAIL.test(out.email.trim())) {
71
+ out.email = out.email.trim().toLowerCase();
72
+ }
73
+ return out;
74
+ }
75
+ function wireOf(params, appId) {
76
+ const items = Array.isArray(params === null || params === void 0 ? void 0 : params.items)
77
+ ? params.items.map((item) => item && typeof item === "object"
78
+ ? {
79
+ ...item,
80
+ variant_id: typeof item.variant_id === "string" ? item.variant_id.toLowerCase() : item.variant_id,
81
+ // Only a quantity that was left out: null or anything else odd
82
+ // goes to the backend as it is and is refused there.
83
+ quantity: item.quantity === undefined ? 1 : item.quantity,
84
+ }
85
+ : item)
86
+ : params === null || params === void 0 ? void 0 : params.items;
87
+ // The site the order is placed on, unless the page names another (or none).
88
+ const app_id = (params === null || params === void 0 ? void 0 : params.app_id) === undefined ? appId : params.app_id;
89
+ return canonical({ ...params, app_id, items, buyer: tidyBuyer(params === null || params === void 0 ? void 0 : params.buyer) });
90
+ }
91
+ /**
92
+ * Creates the orders module for the Doany SDK.
93
+ *
94
+ * Two things are handled here so that page code does not have to:
95
+ *
96
+ * - **Retries do not double-order.** The backend requires an `Idempotency-Key`.
97
+ * Identical calls that overlap are one request (a double click), and a key
98
+ * is kept for an identical order for as long as its outcome is unknown (it
99
+ * failed without an answer), so a retry after a dropped connection lands on
100
+ * the same order — also after a page reload, for 15 minutes. Once the
101
+ * server has answered, the next identical order is a new one.
102
+ * - **A guest can reopen their order.** The order's access token is kept for
103
+ * the browser session and sent with `get` / `cancel` for that order, and
104
+ * with `payments.checkout` / `payments.getForOrder` (order-access.ts).
105
+ *
106
+ * @internal
107
+ */
108
+ export function createOrdersModule(axios, appId, project, {
109
+ // Off for the service role: a backend function's runtime serves many
110
+ // invocations under the same app key, and state shared between them would
111
+ // hand one invocation another's order, or swallow an order as a duplicate.
112
+ rememberAttempts = true, } = {}) {
113
+ const orders = (suffix = "") => project.path(`/orders${suffix}`);
114
+ // Keys of orders whose outcome is unknown. In memory, and mirrored to
115
+ // sessionStorage so that reloading the page after a failed request — the
116
+ // usual way people recover — still retries with the same key.
117
+ const fresh = (at) => typeof at === "number" && Date.now() - at < ATTEMPT_TTL_MS;
118
+ const attemptKey = (fingerprint) => `doany_order_attempt_${appId}_${nameOf(fingerprint)}`;
119
+ const undecided = {
120
+ get(fingerprint) {
121
+ var _a, _b;
122
+ // The window applies wherever the key is held: a tab left open is not a
123
+ // reason to answer a new order with an old one.
124
+ const held = memory.get(fingerprint);
125
+ if (held && fresh(held.at))
126
+ return held.key;
127
+ memory.delete(fingerprint);
128
+ try {
129
+ const raw = (_a = session()) === null || _a === void 0 ? void 0 : _a.getItem(attemptKey(fingerprint));
130
+ if (!raw)
131
+ return undefined;
132
+ const saved = JSON.parse(raw);
133
+ if (saved.key && fresh(saved.at))
134
+ return saved.key;
135
+ (_b = session()) === null || _b === void 0 ? void 0 : _b.removeItem(attemptKey(fingerprint));
136
+ }
137
+ catch (_c) {
138
+ /* unreadable storage is the same as nothing stored */
139
+ }
140
+ return undefined;
141
+ },
142
+ set(fingerprint, key) {
143
+ var _a;
144
+ const entry = { key, at: Date.now() };
145
+ memory.set(fingerprint, entry);
146
+ try {
147
+ (_a = session()) === null || _a === void 0 ? void 0 : _a.setItem(attemptKey(fingerprint), JSON.stringify(entry));
148
+ }
149
+ catch (_b) {
150
+ /* best effort */
151
+ }
152
+ },
153
+ delete(fingerprint) {
154
+ var _a;
155
+ memory.delete(fingerprint);
156
+ try {
157
+ (_a = session()) === null || _a === void 0 ? void 0 : _a.removeItem(attemptKey(fingerprint));
158
+ }
159
+ catch (_b) {
160
+ /* best effort */
161
+ }
162
+ },
163
+ };
164
+ const accessHeaders = (orderId, accessToken) => orderAccessHeaders(axios, appId, orderId, accessToken);
165
+ return {
166
+ create(params, options) {
167
+ if (!rememberAttempts) {
168
+ // One call, one key: the caller's, else a new one. A function that
169
+ // retries passes its own key.
170
+ return (async () => {
171
+ var _a, _b;
172
+ const data = (await axios.request({
173
+ method: "POST",
174
+ url: await orders(),
175
+ data: wireOf(params, appId),
176
+ headers: { "Idempotency-Key": (_a = options === null || options === void 0 ? void 0 : options.idempotencyKey) !== null && _a !== void 0 ? _a : newKey() },
177
+ }));
178
+ if (typeof ((_b = data === null || data === void 0 ? void 0 : data.order) === null || _b === void 0 ? void 0 : _b.id) !== "string") {
179
+ throw new Error("The order service answered without an order");
180
+ }
181
+ return data;
182
+ })();
183
+ }
184
+ return (async () => {
185
+ var _a, _b, _c, _d, _e;
186
+ // The actor is read after the path, with nothing awaited between it and
187
+ // the request: the request goes out as this actor, and its token must be
188
+ // kept for this actor even if the page signs in as someone else before
189
+ // the answer arrives. (Read before the path, a sign-in during the first
190
+ // public-settings read would file the order under the previous account.)
191
+ const url = await orders();
192
+ const actor = actorOf(axios);
193
+ const wire = wireOf(params, appId);
194
+ const fingerprint = JSON.stringify([(_b = (_a = axios.defaults) === null || _a === void 0 ? void 0 : _a.baseURL) !== null && _b !== void 0 ? _b : "", appId, actor, wire]);
195
+ const flight = `${(_c = options === null || options === void 0 ? void 0 : options.idempotencyKey) !== null && _c !== void 0 ? _c : ""}\u0000${fingerprint}`;
196
+ // A second identical call while the first is still out is the same
197
+ // attempt (a double click): it gets the first one's answer, not a
198
+ // request of its own.
199
+ const running = inFlight.get(flight);
200
+ if (running)
201
+ return running;
202
+ const key = (_e = (_d = options === null || options === void 0 ? void 0 : options.idempotencyKey) !== null && _d !== void 0 ? _d : undecided.get(fingerprint)) !== null && _e !== void 0 ? _e : newKey();
203
+ // Written on every attempt, not only the first: the window runs from the
204
+ // LAST time this key went out, or a retry late in it would leave the next
205
+ // one without the key of an order that may just have been placed.
206
+ if (!(options === null || options === void 0 ? void 0 : options.idempotencyKey))
207
+ undecided.set(fingerprint, key);
208
+ // Only the attempt that put a key there may take it away: a page-keyed
209
+ // attempt for the same things must not release the automatic one's key.
210
+ const settle = () => {
211
+ if (!(options === null || options === void 0 ? void 0 : options.idempotencyKey) && undecided.get(fingerprint) === key) {
212
+ undecided.delete(fingerprint);
213
+ }
214
+ };
215
+ const attempt = (async () => {
216
+ var _a;
217
+ try {
218
+ const data = (await axios.request({
219
+ method: "POST",
220
+ url,
221
+ data: wire,
222
+ headers: { "Idempotency-Key": key },
223
+ }));
224
+ // A 2xx that does not carry an order (a proxy's page, a mismatched
225
+ // deployment) decides nothing: the key stays for the retry.
226
+ if (typeof ((_a = data === null || data === void 0 ? void 0 : data.order) === null || _a === void 0 ? void 0 : _a.id) !== "string") {
227
+ throw new Error("The order service answered without an order");
228
+ }
229
+ settle();
230
+ rememberOrderToken(axios, appId, actor, data.order.id, data.access_token);
231
+ return data;
232
+ }
233
+ catch (error) {
234
+ // A 4xx is a decision (refused, invalid) — except 408 and 429, and
235
+ // 401, which the rest of the client also treats as "try again"
236
+ // (client.ts): a proxy can time out AFTER passing the request on.
237
+ // Everything else may have placed the order, so the same key goes
238
+ // out again next time; keeping a key too long costs nothing.
239
+ const status = error === null || error === void 0 ? void 0 : error.status;
240
+ if (typeof status === "number" &&
241
+ status >= 400 &&
242
+ status < 500 &&
243
+ ![401, 408, 429].includes(status)) {
244
+ settle();
245
+ }
246
+ else if (!(options === null || options === void 0 ? void 0 : options.idempotencyKey)) {
247
+ // Still undecided: the window runs from now, however long the
248
+ // request itself was out.
249
+ undecided.set(fingerprint, key);
250
+ }
251
+ throw error;
252
+ }
253
+ finally {
254
+ inFlight.delete(flight);
255
+ }
256
+ })();
257
+ inFlight.set(flight, attempt);
258
+ return attempt;
259
+ })();
260
+ },
261
+ async list(params = {}) {
262
+ return (await axios.get(await orders(), { params: queryOf(params) }));
263
+ },
264
+ async get(orderId, options) {
265
+ return (await axios.get(await orders(`/${seg(orderId)}`), {
266
+ headers: accessHeaders(orderId, options === null || options === void 0 ? void 0 : options.accessToken),
267
+ }));
268
+ },
269
+ async update(orderId, params) {
270
+ return (await axios.patch(await orders(`/${seg(orderId)}`), params));
271
+ },
272
+ async cancel(orderId, options) {
273
+ const version = options === null || options === void 0 ? void 0 : options.version;
274
+ return (await axios.request({
275
+ method: "POST",
276
+ url: await orders(`/${seg(orderId)}/cancel`),
277
+ // The version is optional here: a customer's cancel sends none.
278
+ data: version === undefined ? undefined : { version },
279
+ headers: accessHeaders(orderId, options === null || options === void 0 ? void 0 : options.accessToken),
280
+ }));
281
+ },
282
+ async setFulfillment(orderId, { status, version }) {
283
+ return (await axios.put(await orders(`/${seg(orderId)}/fulfillment`), {
284
+ status,
285
+ version,
286
+ }));
287
+ },
288
+ async complete(orderId, { version }) {
289
+ return (await axios.request({
290
+ method: "POST",
291
+ url: await orders(`/${seg(orderId)}/complete`),
292
+ data: { version },
293
+ }));
294
+ },
295
+ };
296
+ }