@curless/shopify-storefront 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/card-contract.test.d.ts +2 -0
- package/dist/__tests__/card-contract.test.d.ts.map +1 -0
- package/dist/__tests__/card-contract.test.js +22 -0
- package/dist/__tests__/card-contract.test.js.map +1 -0
- package/dist/__tests__/catalog-registry.test.d.ts +2 -0
- package/dist/__tests__/catalog-registry.test.d.ts.map +1 -0
- package/dist/__tests__/catalog-registry.test.js +109 -0
- package/dist/__tests__/catalog-registry.test.js.map +1 -0
- package/dist/__tests__/challenge.test.d.ts +2 -0
- package/dist/__tests__/challenge.test.d.ts.map +1 -0
- package/dist/__tests__/challenge.test.js +46 -0
- package/dist/__tests__/challenge.test.js.map +1 -0
- package/dist/__tests__/config.test.d.ts +2 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +123 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/connection-state.test.d.ts +2 -0
- package/dist/__tests__/connection-state.test.d.ts.map +1 -0
- package/dist/__tests__/connection-state.test.js +40 -0
- package/dist/__tests__/connection-state.test.js.map +1 -0
- package/dist/__tests__/multi-token-store.test.d.ts +2 -0
- package/dist/__tests__/multi-token-store.test.d.ts.map +1 -0
- package/dist/__tests__/multi-token-store.test.js +89 -0
- package/dist/__tests__/multi-token-store.test.js.map +1 -0
- package/dist/__tests__/oauth.test.d.ts +2 -0
- package/dist/__tests__/oauth.test.d.ts.map +1 -0
- package/dist/__tests__/oauth.test.js +112 -0
- package/dist/__tests__/oauth.test.js.map +1 -0
- package/dist/__tests__/output-schema.test.d.ts +2 -0
- package/dist/__tests__/output-schema.test.d.ts.map +1 -0
- package/dist/__tests__/output-schema.test.js +112 -0
- package/dist/__tests__/output-schema.test.js.map +1 -0
- package/dist/__tests__/ping.test.d.ts +2 -0
- package/dist/__tests__/ping.test.d.ts.map +1 -0
- package/dist/__tests__/ping.test.js +22 -0
- package/dist/__tests__/ping.test.js.map +1 -0
- package/dist/__tests__/rate-limit.test.d.ts +2 -0
- package/dist/__tests__/rate-limit.test.d.ts.map +1 -0
- package/dist/__tests__/rate-limit.test.js +46 -0
- package/dist/__tests__/rate-limit.test.js.map +1 -0
- package/dist/__tests__/stock.test.d.ts +2 -0
- package/dist/__tests__/stock.test.d.ts.map +1 -0
- package/dist/__tests__/stock.test.js +109 -0
- package/dist/__tests__/stock.test.js.map +1 -0
- package/dist/__tests__/tenant.test.d.ts +2 -0
- package/dist/__tests__/tenant.test.d.ts.map +1 -0
- package/dist/__tests__/tenant.test.js +105 -0
- package/dist/__tests__/tenant.test.js.map +1 -0
- package/dist/__tests__/tools.test.d.ts +2 -0
- package/dist/__tests__/tools.test.d.ts.map +1 -0
- package/dist/__tests__/tools.test.js +543 -0
- package/dist/__tests__/tools.test.js.map +1 -0
- package/dist/__tests__/upstream-cap.test.d.ts +2 -0
- package/dist/__tests__/upstream-cap.test.d.ts.map +1 -0
- package/dist/__tests__/upstream-cap.test.js +78 -0
- package/dist/__tests__/upstream-cap.test.js.map +1 -0
- package/dist/__tests__/webhooks.test.d.ts +2 -0
- package/dist/__tests__/webhooks.test.d.ts.map +1 -0
- package/dist/__tests__/webhooks.test.js +88 -0
- package/dist/__tests__/webhooks.test.js.map +1 -0
- package/dist/card.generated.d.ts +2 -0
- package/dist/card.generated.d.ts.map +1 -0
- package/dist/card.generated.js +6 -0
- package/dist/card.generated.js.map +1 -0
- package/dist/catalog-registry.d.ts +44 -0
- package/dist/catalog-registry.d.ts.map +1 -0
- package/dist/catalog-registry.js +105 -0
- package/dist/catalog-registry.js.map +1 -0
- package/dist/catalog.d.ts +51 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +189 -0
- package/dist/catalog.js.map +1 -0
- package/dist/challenge.d.ts +11 -0
- package/dist/challenge.d.ts.map +1 -0
- package/dist/challenge.js +36 -0
- package/dist/challenge.js.map +1 -0
- package/dist/config.d.ts +141 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +65 -0
- package/dist/config.js.map +1 -0
- package/dist/connection.d.ts +10 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +21 -0
- package/dist/connection.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +974 -0
- package/dist/index.js.map +1 -0
- package/dist/legal.d.ts +52 -0
- package/dist/legal.d.ts.map +1 -0
- package/dist/legal.js +139 -0
- package/dist/legal.js.map +1 -0
- package/dist/load-config.d.ts +11 -0
- package/dist/load-config.d.ts.map +1 -0
- package/dist/load-config.js +23 -0
- package/dist/load-config.js.map +1 -0
- package/dist/multi-token-store.d.ts +18 -0
- package/dist/multi-token-store.d.ts.map +1 -0
- package/dist/multi-token-store.js +95 -0
- package/dist/multi-token-store.js.map +1 -0
- package/dist/oauth.d.ts +20 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +123 -0
- package/dist/oauth.js.map +1 -0
- package/dist/rate-limit.d.ts +15 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +56 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/stock.d.ts +39 -0
- package/dist/stock.d.ts.map +1 -0
- package/dist/stock.js +77 -0
- package/dist/stock.js.map +1 -0
- package/dist/tenant.d.ts +69 -0
- package/dist/tenant.d.ts.map +1 -0
- package/dist/tenant.js +68 -0
- package/dist/tenant.js.map +1 -0
- package/dist/token-store.d.ts +16 -0
- package/dist/token-store.d.ts.map +1 -0
- package/dist/token-store.js +47 -0
- package/dist/token-store.js.map +1 -0
- package/dist/tools.d.ts +201 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +779 -0
- package/dist/tools.js.map +1 -0
- package/dist/webhooks.d.ts +30 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +69 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export class RateLimiter {
|
|
2
|
+
limit;
|
|
3
|
+
windowMs;
|
|
4
|
+
maxKeys;
|
|
5
|
+
buckets = new Map();
|
|
6
|
+
constructor(limit, windowMs,
|
|
7
|
+
/** Bound the map so a flood of distinct IPs cannot grow it without limit. */
|
|
8
|
+
maxKeys = 10_000) {
|
|
9
|
+
this.limit = limit;
|
|
10
|
+
this.windowMs = windowMs;
|
|
11
|
+
this.maxKeys = maxKeys;
|
|
12
|
+
}
|
|
13
|
+
/** True when the call is allowed. */
|
|
14
|
+
take(key, now = Date.now()) {
|
|
15
|
+
const found = this.buckets.get(key);
|
|
16
|
+
if (!found || now >= found.resetAt) {
|
|
17
|
+
if (this.buckets.size >= this.maxKeys)
|
|
18
|
+
this.evictExpired(now);
|
|
19
|
+
this.buckets.set(key, { count: 1, resetAt: now + this.windowMs });
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
if (found.count >= this.limit)
|
|
23
|
+
return false;
|
|
24
|
+
found.count += 1;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
evictExpired(now) {
|
|
28
|
+
for (const [k, b] of this.buckets) {
|
|
29
|
+
if (now >= b.resetAt)
|
|
30
|
+
this.buckets.delete(k);
|
|
31
|
+
}
|
|
32
|
+
// Still full of live buckets — this is a flood, not accumulation. Drop the
|
|
33
|
+
// oldest rather than grow: refusing to track is the same as rate limiting.
|
|
34
|
+
if (this.buckets.size >= this.maxKeys) {
|
|
35
|
+
const oldest = [...this.buckets.entries()]
|
|
36
|
+
.sort((a, b) => a[1].resetAt - b[1].resetAt)
|
|
37
|
+
.slice(0, Math.floor(this.maxKeys / 4));
|
|
38
|
+
for (const [k] of oldest)
|
|
39
|
+
this.buckets.delete(k);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// The caller's address as seen through Caddy. `x-forwarded-for` is a list; the
|
|
44
|
+
// FIRST entry is the client, the rest are proxies. It is spoofable in general —
|
|
45
|
+
// but our only ingress sets it, so the last hop is ours and the first entry is
|
|
46
|
+
// what Caddy saw.
|
|
47
|
+
export const clientIp = (headers, fallback = 'unknown') => {
|
|
48
|
+
const xff = headers.get('x-forwarded-for');
|
|
49
|
+
if (xff) {
|
|
50
|
+
const first = xff.split(',')[0]?.trim();
|
|
51
|
+
if (first)
|
|
52
|
+
return first;
|
|
53
|
+
}
|
|
54
|
+
return headers.get('x-real-ip')?.trim() || fallback;
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAiBA,MAAM,OAAO,WAAW;IAIH;IACA;IAEA;IANX,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE5C,YACmB,KAAa,EACb,QAAgB;IACjC,6EAA6E;IAC5D,UAAU,MAAM;QAHhB,UAAK,GAAL,KAAK,CAAQ;QACb,aAAQ,GAAR,QAAQ,CAAQ;QAEhB,YAAO,GAAP,OAAO,CAAS;IAChC,CAAC;IAEJ,qCAAqC;IACrC,IAAI,CAAC,GAAW,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;QACxC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACpC,IAAI,CAAC,KAAK,IAAI,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;YACnC,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,OAAO;gBAAE,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;YAC9D,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;YAClE,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,KAAK,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO,KAAK,CAAC;QAC5C,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;QACjB,OAAO,IAAI,CAAC;IACd,CAAC;IAEO,YAAY,CAAC,GAAW;QAC9B,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAClC,IAAI,GAAG,IAAI,CAAC,CAAC,OAAO;gBAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC/C,CAAC;QACD,2EAA2E;QAC3E,2EAA2E;QAC3E,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACtC,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;iBACvC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;iBAC3C,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC;YAC1C,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,MAAM;gBAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;CACF;AAED,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAC/E,kBAAkB;AAClB,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,OAAgB,EAAE,QAAQ,GAAG,SAAS,EAAU,EAAE;IACzE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAC3C,IAAI,GAAG,EAAE,CAAC;QACR,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;QACxC,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC;IAC1B,CAAC;IACD,OAAO,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,IAAI,QAAQ,CAAC;AACtD,CAAC,CAAC"}
|
package/dist/stock.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** One buyable combination, e.g. `{Left: '-5.75', Right: '-5.50'}`. */
|
|
2
|
+
export type Combo = Record<string, string>;
|
|
3
|
+
/**
|
|
4
|
+
* The buyable combinations out of a tool result, from wherever that result put
|
|
5
|
+
* them.
|
|
6
|
+
*
|
|
7
|
+
* The two views disagree, and reasonably: `get_moody_lens` nests everything
|
|
8
|
+
* about the lens under `product`, while `needs-options` keeps the answer to
|
|
9
|
+
* "what is still missing" at the top level beside `given` and `options`. The
|
|
10
|
+
* card read only the top level, so on the product view `stock` was always empty
|
|
11
|
+
* — and an empty list means "we do not know", which offers everything. Every
|
|
12
|
+
* prescription looked available at a store with 2 of 676 in stock.
|
|
13
|
+
*
|
|
14
|
+
* So the lookup lives here, next to the rule that consumes it, and is pinned by
|
|
15
|
+
* a test against BOTH shapes. Reading the wrong one is silent: nothing throws,
|
|
16
|
+
* nothing looks broken, the buyer is just quietly told they can have anything.
|
|
17
|
+
*/
|
|
18
|
+
export declare const stockOf: (sc: any) => Combo[];
|
|
19
|
+
/**
|
|
20
|
+
* True when `value` for option `name` still leads to something buyable.
|
|
21
|
+
*
|
|
22
|
+
* A value survives when SOME in-stock combination has it and agrees with every
|
|
23
|
+
* OTHER choice already made — so picking a left eye narrows the right, which is
|
|
24
|
+
* how the two-eye matrix actually works. The option being asked about is excluded
|
|
25
|
+
* from that agreement check, or re-picking within a group could never widen it.
|
|
26
|
+
*
|
|
27
|
+
* An EMPTY stock list means "we do not know" — from a payload the server had to
|
|
28
|
+
* truncate — and everything is offered. Filtering on a partial list would hide
|
|
29
|
+
* prescriptions the store has, which is worse than the sold-out message: an
|
|
30
|
+
* invisible option cannot be argued with.
|
|
31
|
+
*/
|
|
32
|
+
export declare const offeredValue: (stock: Combo[], chosen: Combo, name: string, value: string) => boolean;
|
|
33
|
+
/**
|
|
34
|
+
* The choices to keep after a new pick. Anything the latest pick has made
|
|
35
|
+
* impossible is dropped, so the buyer is never holding a combination the store
|
|
36
|
+
* does not have — and never sees Buy go live over one.
|
|
37
|
+
*/
|
|
38
|
+
export declare const pruneChoices: (stock: Combo[], chosen: Combo) => Combo;
|
|
39
|
+
//# sourceMappingURL=stock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stock.d.ts","sourceRoot":"","sources":["../src/stock.ts"],"names":[],"mappings":"AAeA,uEAAuE;AACvE,MAAM,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE3C;;;;;;;;;;;;;;GAcG;AAEH,eAAO,MAAM,OAAO,GAAI,IAAI,GAAG,KAAG,KAAK,EAOtC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,YAAY,GACvB,OAAO,KAAK,EAAE,EACd,QAAQ,KAAK,EACb,MAAM,MAAM,EACZ,OAAO,MAAM,KACZ,OAOF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,KAAK,EAAE,EAAE,QAAQ,KAAK,KAAG,KAU5D,CAAC"}
|
package/dist/stock.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Which option values a buyer can still pick, given what the store actually has.
|
|
2
|
+
//
|
|
3
|
+
// This is the whole difference between a picker that works and one that wastes
|
|
4
|
+
// the buyer's time. The option lists say what the store OFFERS — 26 left powers
|
|
5
|
+
// × 26 right powers — and cannot say which PAIRS exist. At Moody, 674 of those
|
|
6
|
+
// 676 pairs do not, so a picker built on the option lists alone tells the buyer
|
|
7
|
+
// "sold out" on almost every choice they make.
|
|
8
|
+
//
|
|
9
|
+
// It lives on the SERVER side and the card imports it across the folder
|
|
10
|
+
// boundary, because it is one half of a contract with the other: the server
|
|
11
|
+
// decides what goes in `inStock`, the card decides what to do with it, and the
|
|
12
|
+
// two disagreeing is invisible until a buyer is told the wrong thing. Here it is
|
|
13
|
+
// plain, pure and unit-tested, rather than only checkable by clicking through a
|
|
14
|
+
// live store.
|
|
15
|
+
/**
|
|
16
|
+
* The buyable combinations out of a tool result, from wherever that result put
|
|
17
|
+
* them.
|
|
18
|
+
*
|
|
19
|
+
* The two views disagree, and reasonably: `get_moody_lens` nests everything
|
|
20
|
+
* about the lens under `product`, while `needs-options` keeps the answer to
|
|
21
|
+
* "what is still missing" at the top level beside `given` and `options`. The
|
|
22
|
+
* card read only the top level, so on the product view `stock` was always empty
|
|
23
|
+
* — and an empty list means "we do not know", which offers everything. Every
|
|
24
|
+
* prescription looked available at a store with 2 of 676 in stock.
|
|
25
|
+
*
|
|
26
|
+
* So the lookup lives here, next to the rule that consumes it, and is pinned by
|
|
27
|
+
* a test against BOTH shapes. Reading the wrong one is silent: nothing throws,
|
|
28
|
+
* nothing looks broken, the buyer is just quietly told they can have anything.
|
|
29
|
+
*/
|
|
30
|
+
// biome-ignore lint/suspicious/noExplicitAny: a server-shaped tool payload
|
|
31
|
+
export const stockOf = (sc) => {
|
|
32
|
+
const at = sc?.inStock !== undefined ? sc : sc?.product;
|
|
33
|
+
// Truncated means the server had more than it would send. Filtering on a
|
|
34
|
+
// partial list would HIDE prescriptions the store has, which is worse than
|
|
35
|
+
// the sold-out message: an invisible option cannot be argued with.
|
|
36
|
+
if (!at || at.inStockTruncated)
|
|
37
|
+
return [];
|
|
38
|
+
return at.inStock ?? [];
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* True when `value` for option `name` still leads to something buyable.
|
|
42
|
+
*
|
|
43
|
+
* A value survives when SOME in-stock combination has it and agrees with every
|
|
44
|
+
* OTHER choice already made — so picking a left eye narrows the right, which is
|
|
45
|
+
* how the two-eye matrix actually works. The option being asked about is excluded
|
|
46
|
+
* from that agreement check, or re-picking within a group could never widen it.
|
|
47
|
+
*
|
|
48
|
+
* An EMPTY stock list means "we do not know" — from a payload the server had to
|
|
49
|
+
* truncate — and everything is offered. Filtering on a partial list would hide
|
|
50
|
+
* prescriptions the store has, which is worse than the sold-out message: an
|
|
51
|
+
* invisible option cannot be argued with.
|
|
52
|
+
*/
|
|
53
|
+
export const offeredValue = (stock, chosen, name, value) => {
|
|
54
|
+
if (stock.length === 0)
|
|
55
|
+
return true;
|
|
56
|
+
return stock.some((combo) => combo[name] === value &&
|
|
57
|
+
Object.entries(chosen).every(([k, v]) => k === name || combo[k] === v));
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* The choices to keep after a new pick. Anything the latest pick has made
|
|
61
|
+
* impossible is dropped, so the buyer is never holding a combination the store
|
|
62
|
+
* does not have — and never sees Buy go live over one.
|
|
63
|
+
*/
|
|
64
|
+
export const pruneChoices = (stock, chosen) => {
|
|
65
|
+
if (stock.length === 0)
|
|
66
|
+
return { ...chosen };
|
|
67
|
+
const kept = { ...chosen };
|
|
68
|
+
for (const name of Object.keys(chosen)) {
|
|
69
|
+
const rest = { ...kept };
|
|
70
|
+
delete rest[name];
|
|
71
|
+
const v = chosen[name];
|
|
72
|
+
if (v === undefined || !offeredValue(stock, rest, name, v))
|
|
73
|
+
delete kept[name];
|
|
74
|
+
}
|
|
75
|
+
return kept;
|
|
76
|
+
};
|
|
77
|
+
//# sourceMappingURL=stock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stock.js","sourceRoot":"","sources":["../src/stock.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAC/E,gFAAgF;AAChF,+CAA+C;AAC/C,EAAE;AACF,wEAAwE;AACxE,4EAA4E;AAC5E,+EAA+E;AAC/E,iFAAiF;AACjF,gFAAgF;AAChF,cAAc;AAKd;;;;;;;;;;;;;;GAcG;AACH,2EAA2E;AAC3E,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,EAAO,EAAW,EAAE;IAC1C,MAAM,EAAE,GAAG,EAAE,EAAE,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,OAAO,CAAC;IACxD,yEAAyE;IACzE,2EAA2E;IAC3E,mEAAmE;IACnE,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,gBAAgB;QAAE,OAAO,EAAE,CAAC;IAC1C,OAAQ,EAAE,CAAC,OAA+B,IAAI,EAAE,CAAC;AACnD,CAAC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAC1B,KAAc,EACd,MAAa,EACb,IAAY,EACZ,KAAa,EACJ,EAAE;IACX,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,OAAO,KAAK,CAAC,IAAI,CACf,CAAC,KAAK,EAAE,EAAE,CACR,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK;QACrB,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CACzE,CAAC;AACJ,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,KAAc,EAAE,MAAa,EAAS,EAAE;IACnE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,GAAG,MAAM,EAAE,CAAC;IAC7C,MAAM,IAAI,GAAU,EAAE,GAAG,MAAM,EAAE,CAAC;IAClC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,GAAG,EAAE,GAAG,IAAI,EAAE,CAAC;QACzB,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC;QAClB,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QACvB,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC;IAChF,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC,CAAC"}
|
package/dist/tenant.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ShopifyConfig } from '@curless/shopify-adapter';
|
|
2
|
+
import type { CatalogRegistry } from './catalog-registry.js';
|
|
3
|
+
import type { Catalog } from './catalog.js';
|
|
4
|
+
import type { ResolvedStorefront } from './config.js';
|
|
5
|
+
import type { MultiTokenStore } from './multi-token-store.js';
|
|
6
|
+
import type { TokenStore } from './token-store.js';
|
|
7
|
+
export type Tenant = {
|
|
8
|
+
shopDomain: string;
|
|
9
|
+
catalog: Catalog;
|
|
10
|
+
/**
|
|
11
|
+
* The Shopify credentials for THIS tenant.
|
|
12
|
+
*
|
|
13
|
+
* Returned by the resolver rather than fetched again by the caller: the
|
|
14
|
+
* resolver already read the token to decide whether the shop is installed,
|
|
15
|
+
* and a second lookup is a second chance to look it up for a different shop
|
|
16
|
+
* than the one just resolved.
|
|
17
|
+
*/
|
|
18
|
+
shopify: ShopifyConfig;
|
|
19
|
+
};
|
|
20
|
+
/** Why no tenant — the caller decides what a buyer should see for each. */
|
|
21
|
+
export type NoTenant = {
|
|
22
|
+
reason: 'no-shop';
|
|
23
|
+
} | {
|
|
24
|
+
reason: 'bad-shop';
|
|
25
|
+
given: string;
|
|
26
|
+
} | {
|
|
27
|
+
reason: 'not-installed';
|
|
28
|
+
shop: string;
|
|
29
|
+
};
|
|
30
|
+
export type Resolution = {
|
|
31
|
+
ok: true;
|
|
32
|
+
tenant: Tenant;
|
|
33
|
+
} | ({
|
|
34
|
+
ok: false;
|
|
35
|
+
} & NoTenant);
|
|
36
|
+
export type TokenReader = Pick<TokenStore, 'get'>;
|
|
37
|
+
export type MultiTokenReader = Pick<MultiTokenStore, 'get'>;
|
|
38
|
+
export type CatalogSource = Pick<CatalogRegistry, 'for'>;
|
|
39
|
+
export type SingleShopDeps = {
|
|
40
|
+
store: ResolvedStorefront;
|
|
41
|
+
catalog: Catalog;
|
|
42
|
+
tokens: TokenReader;
|
|
43
|
+
};
|
|
44
|
+
export type MultiShopDeps = {
|
|
45
|
+
registry: CatalogSource;
|
|
46
|
+
tokens: MultiTokenReader;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* The branded case. The shop is fixed, so a `shop` on the request is not a
|
|
50
|
+
* question to answer — it is a claim to CHECK. A request naming a different
|
|
51
|
+
* shop than this deployment serves is refused rather than quietly served the
|
|
52
|
+
* only catalogue we have, because the caller asked about someone else.
|
|
53
|
+
*/
|
|
54
|
+
export declare const singleShopResolver: (deps: SingleShopDeps) => (requestedShop?: string) => Promise<Resolution>;
|
|
55
|
+
/**
|
|
56
|
+
* The listed case. Every answer here depends on a value from the request, so
|
|
57
|
+
* each step refuses rather than guesses:
|
|
58
|
+
*
|
|
59
|
+
* • no shop at all — there is no default, and picking one would answer as
|
|
60
|
+
* whichever merchant happened to be first
|
|
61
|
+
* • a shop we will not name — see multi-token-store; it is about to be a
|
|
62
|
+
* filename
|
|
63
|
+
* • a shop with no token — not installed HERE, which is an ordinary state
|
|
64
|
+
* and not an error
|
|
65
|
+
*/
|
|
66
|
+
export declare const multiShopResolver: (deps: MultiShopDeps) => (requestedShop?: string) => Promise<Resolution>;
|
|
67
|
+
/** One line a buyer can read, and one an operator can act on. */
|
|
68
|
+
export declare const explainNoTenant: (r: NoTenant) => string;
|
|
69
|
+
//# sourceMappingURL=tenant.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../src/tenant.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,MAAM,MAAM,GAAG;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB;;;;;;;OAOG;IACH,OAAO,EAAE,aAAa,CAAC;CACxB,CAAC;AAEF,2EAA2E;AAC3E,MAAM,MAAM,QAAQ,GAChB;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,MAAM,EAAE,eAAe,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9C,MAAM,MAAM,UAAU,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,CAAC;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,GAAG,QAAQ,CAAC,CAAC;AAUnF,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;AAClD,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,eAAe,EAAE,KAAK,CAAC,CAAC;AAC5D,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,eAAe,EAAE,KAAK,CAAC,CAAC;AAEzD,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,kBAAkB,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,WAAW,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,aAAa,CAAC;IACxB,MAAM,EAAE,gBAAgB,CAAC;CAC1B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAC5B,MAAM,cAAc,MACd,gBAAgB,MAAM,KAAG,OAAO,CAAC,UAAU,CAkBjD,CAAC;AAEJ;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,GAC3B,MAAM,aAAa,MACb,gBAAgB,MAAM,KAAG,OAAO,CAAC,UAAU,CAcjD,CAAC;AAEJ,iEAAiE;AACjE,eAAO,MAAM,eAAe,GAAI,GAAG,QAAQ,KAAG,MAS7C,CAAC"}
|
package/dist/tenant.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { isStorableShop } from './multi-token-store.js';
|
|
2
|
+
/**
|
|
3
|
+
* The branded case. The shop is fixed, so a `shop` on the request is not a
|
|
4
|
+
* question to answer — it is a claim to CHECK. A request naming a different
|
|
5
|
+
* shop than this deployment serves is refused rather than quietly served the
|
|
6
|
+
* only catalogue we have, because the caller asked about someone else.
|
|
7
|
+
*/
|
|
8
|
+
export const singleShopResolver = (deps) => async (requestedShop) => {
|
|
9
|
+
const configured = (deps.store.config.shopify.shopDomain ?? '').toLowerCase();
|
|
10
|
+
if (requestedShop && requestedShop.toLowerCase() !== configured) {
|
|
11
|
+
return { ok: false, reason: 'bad-shop', given: requestedShop };
|
|
12
|
+
}
|
|
13
|
+
// An env-supplied admin token skips the install entirely — the branded
|
|
14
|
+
// deployments used it before a merchant ever clicked anything.
|
|
15
|
+
const envToken = process.env.STOREFRONT_SHOPIFY_ADMIN_TOKEN;
|
|
16
|
+
const stored = envToken ? { accessToken: envToken } : await deps.tokens.get();
|
|
17
|
+
if (!stored)
|
|
18
|
+
return { ok: false, reason: 'not-installed', shop: configured };
|
|
19
|
+
return {
|
|
20
|
+
ok: true,
|
|
21
|
+
tenant: {
|
|
22
|
+
shopDomain: configured,
|
|
23
|
+
catalog: deps.catalog,
|
|
24
|
+
shopify: { shopDomain: configured, adminToken: stored.accessToken },
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* The listed case. Every answer here depends on a value from the request, so
|
|
30
|
+
* each step refuses rather than guesses:
|
|
31
|
+
*
|
|
32
|
+
* • no shop at all — there is no default, and picking one would answer as
|
|
33
|
+
* whichever merchant happened to be first
|
|
34
|
+
* • a shop we will not name — see multi-token-store; it is about to be a
|
|
35
|
+
* filename
|
|
36
|
+
* • a shop with no token — not installed HERE, which is an ordinary state
|
|
37
|
+
* and not an error
|
|
38
|
+
*/
|
|
39
|
+
export const multiShopResolver = (deps) => async (requestedShop) => {
|
|
40
|
+
const shop = (requestedShop ?? '').trim().toLowerCase();
|
|
41
|
+
if (!shop)
|
|
42
|
+
return { ok: false, reason: 'no-shop' };
|
|
43
|
+
if (!isStorableShop(shop))
|
|
44
|
+
return { ok: false, reason: 'bad-shop', given: shop };
|
|
45
|
+
const stored = await deps.tokens.get(shop);
|
|
46
|
+
if (!stored)
|
|
47
|
+
return { ok: false, reason: 'not-installed', shop };
|
|
48
|
+
return {
|
|
49
|
+
ok: true,
|
|
50
|
+
tenant: {
|
|
51
|
+
shopDomain: shop,
|
|
52
|
+
catalog: deps.registry.for(shop),
|
|
53
|
+
shopify: { shopDomain: shop, adminToken: stored.accessToken },
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
/** One line a buyer can read, and one an operator can act on. */
|
|
58
|
+
export const explainNoTenant = (r) => {
|
|
59
|
+
switch (r.reason) {
|
|
60
|
+
case 'no-shop':
|
|
61
|
+
return 'No shop was named on this request, and this app serves several — say which store.';
|
|
62
|
+
case 'bad-shop':
|
|
63
|
+
return `${r.given} is not a store this connector serves.`;
|
|
64
|
+
case 'not-installed':
|
|
65
|
+
return `${r.shop} has not installed this app yet.`;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=tenant.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant.js","sourceRoot":"","sources":["../src/tenant.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAgDxD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAC7B,CAAC,IAAoB,EAAE,EAAE,CACzB,KAAK,EAAE,aAAsB,EAAuB,EAAE;IACpD,MAAM,UAAU,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IAC9E,IAAI,aAAa,IAAI,aAAa,CAAC,WAAW,EAAE,KAAK,UAAU,EAAE,CAAC;QAChE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC;IACjE,CAAC;IACD,uEAAuE;IACvE,+DAA+D;IAC/D,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC;IAC5D,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;IAC9E,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;IAC7E,OAAO;QACL,EAAE,EAAE,IAAI;QACR,MAAM,EAAE;YACN,UAAU,EAAE,UAAU;YACtB,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,OAAO,EAAE,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,CAAC,WAAW,EAAE;SACpE;KACF,CAAC;AACJ,CAAC,CAAC;AAEJ;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAC5B,CAAC,IAAmB,EAAE,EAAE,CACxB,KAAK,EAAE,aAAsB,EAAuB,EAAE;IACpD,MAAM,IAAI,GAAG,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACxD,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IACnD,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACjF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC3C,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC;IACjE,OAAO;QACL,EAAE,EAAE,IAAI;QACR,MAAM,EAAE;YACN,UAAU,EAAE,IAAI;YAChB,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC;YAChC,OAAO,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,CAAC,WAAW,EAAE;SAC9D;KACF,CAAC;AACJ,CAAC,CAAC;AAEJ,iEAAiE;AACjE,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAW,EAAU,EAAE;IACrD,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC;QACjB,KAAK,SAAS;YACZ,OAAO,mFAAmF,CAAC;QAC7F,KAAK,UAAU;YACb,OAAO,GAAG,CAAC,CAAC,KAAK,wCAAwC,CAAC;QAC5D,KAAK,eAAe;YAClB,OAAO,GAAG,CAAC,CAAC,IAAI,kCAAkC,CAAC;IACvD,CAAC;AACH,CAAC,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type StoredToken = {
|
|
2
|
+
shopDomain: string;
|
|
3
|
+
accessToken: string;
|
|
4
|
+
/** The scopes Shopify actually granted — may differ from what we asked for. */
|
|
5
|
+
scope: string;
|
|
6
|
+
installedAt: string;
|
|
7
|
+
};
|
|
8
|
+
export declare class TokenStore {
|
|
9
|
+
private readonly path;
|
|
10
|
+
private cached;
|
|
11
|
+
private loaded;
|
|
12
|
+
constructor(path: string);
|
|
13
|
+
get(): Promise<StoredToken | null>;
|
|
14
|
+
set(token: StoredToken): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=token-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../src/token-store.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,WAAW,GAAG;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,+EAA+E;IAC/E,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,qBAAa,UAAU;IAIT,OAAO,CAAC,QAAQ,CAAC,IAAI;IAHjC,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,MAAM,CAAS;gBAEM,IAAI,EAAE,MAAM;IAEnC,GAAG,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAelC,GAAG,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;CAU7C"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Where the merchant's access token lives after they install.
|
|
2
|
+
//
|
|
3
|
+
// A file, not a database: this connector deliberately owns no database, and
|
|
4
|
+
// coupling a client's storefront to the payment gateway's Postgres would undo
|
|
5
|
+
// the separation that keeps a client's credentials out of the gateway's blast
|
|
6
|
+
// radius. One shop, one token, one small file.
|
|
7
|
+
//
|
|
8
|
+
// ⚠️ The file IS a credential — it grants read access to a real merchant's
|
|
9
|
+
// products and orders. It must live on a mounted volume (so it survives a
|
|
10
|
+
// redeploy) with the same care as .env, and it is written 0600.
|
|
11
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
12
|
+
import { dirname } from 'node:path';
|
|
13
|
+
export class TokenStore {
|
|
14
|
+
path;
|
|
15
|
+
cached = null;
|
|
16
|
+
loaded = false;
|
|
17
|
+
constructor(path) {
|
|
18
|
+
this.path = path;
|
|
19
|
+
}
|
|
20
|
+
async get() {
|
|
21
|
+
if (this.loaded)
|
|
22
|
+
return this.cached;
|
|
23
|
+
try {
|
|
24
|
+
const raw = await readFile(this.path, 'utf8');
|
|
25
|
+
const parsed = JSON.parse(raw);
|
|
26
|
+
this.cached = parsed.accessToken ? parsed : null;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Absent or unreadable both mean "not installed yet". This is the normal
|
|
30
|
+
// state of a fresh deploy, not an error worth failing boot over.
|
|
31
|
+
this.cached = null;
|
|
32
|
+
}
|
|
33
|
+
this.loaded = true;
|
|
34
|
+
return this.cached;
|
|
35
|
+
}
|
|
36
|
+
async set(token) {
|
|
37
|
+
await mkdir(dirname(this.path), { recursive: true });
|
|
38
|
+
// Write-then-rename: a crash mid-write must not leave a truncated file that
|
|
39
|
+
// reads as "installed" with half a token.
|
|
40
|
+
const tmp = `${this.path}.tmp`;
|
|
41
|
+
await writeFile(tmp, JSON.stringify(token, null, 2), { mode: 0o600 });
|
|
42
|
+
await rename(tmp, this.path);
|
|
43
|
+
this.cached = token;
|
|
44
|
+
this.loaded = true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=token-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-store.js","sourceRoot":"","sources":["../src/token-store.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,8EAA8E;AAC9E,+CAA+C;AAC/C,EAAE;AACF,2EAA2E;AAC3E,0EAA0E;AAC1E,gEAAgE;AAChE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUpC,MAAM,OAAO,UAAU;IAIQ;IAHrB,MAAM,GAAuB,IAAI,CAAC;IAClC,MAAM,GAAG,KAAK,CAAC;IAEvB,YAA6B,IAAY;QAAZ,SAAI,GAAJ,IAAI,CAAQ;IAAG,CAAC;IAE7C,KAAK,CAAC,GAAG;QACP,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC,MAAM,CAAC;QACpC,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAgB,CAAC;YAC9C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;QACnD,CAAC;QAAC,MAAM,CAAC;YACP,yEAAyE;YACzE,iEAAiE;YACjE,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACrB,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,KAAkB;QAC1B,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,4EAA4E;QAC5E,0CAA0C;QAC1C,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,IAAI,MAAM,CAAC;QAC/B,MAAM,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACtE,MAAM,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7B,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;IACrB,CAAC;CACF"}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { type ShopifyConfig, type ShopifyProduct } from '@curless/shopify-adapter';
|
|
2
|
+
import type { Catalog } from './catalog.js';
|
|
3
|
+
import type { ToolNames, Vocabulary } from './config.js';
|
|
4
|
+
/**
|
|
5
|
+
* The tool table, built for THIS storefront's names.
|
|
6
|
+
*
|
|
7
|
+
* A constant until there was a second merchant. A model choosing among
|
|
8
|
+
* several storefront connectors picks by tool name, so the names carry the
|
|
9
|
+
* merchant — and that means they cannot be literals here.
|
|
10
|
+
*/
|
|
11
|
+
export declare const buildTools: (n: ToolNames, v: Vocabulary) => readonly [{
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly description: `Open the ${string} storefront \u2014 the live catalogue of the shop itself: the price it charges today, what it can actually ship, and the combinations it will accept, rendered as a card the buyer can buy from. Call this FIRST, with NO arguments, as soon as the buyer wants ${string} \u2014 it renders a card with pictures, filter chips and a Buy button on every ${string}, so they can browse and choose without being asked anything. Do not interview them first and do not recommend on top of it. Pass \`query\` only once they have said what they want.${string}`;
|
|
14
|
+
readonly annotations: {
|
|
15
|
+
readonly title: string;
|
|
16
|
+
readonly readOnlyHint: true;
|
|
17
|
+
readonly destructiveHint: false;
|
|
18
|
+
readonly openWorldHint: false;
|
|
19
|
+
};
|
|
20
|
+
readonly inputSchema: {
|
|
21
|
+
readonly type: "object";
|
|
22
|
+
readonly properties: {
|
|
23
|
+
readonly query: {
|
|
24
|
+
readonly type: "string";
|
|
25
|
+
readonly description: "Free text over the name, tags and description — this is how colour is searched (\"green\", \"brown\", \"matcha\")";
|
|
26
|
+
};
|
|
27
|
+
readonly maxPrice: {
|
|
28
|
+
readonly type: "number";
|
|
29
|
+
readonly description: "Upper bound in whole currency units";
|
|
30
|
+
};
|
|
31
|
+
readonly limit: {
|
|
32
|
+
readonly type: "number";
|
|
33
|
+
readonly description: "Default 20";
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
}, {
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly description: `One ${string} in full: its options, every value offered, prices and images. Check this BEFORE checkout \u2014 how many choices the buyer must make is set by the product, not fixed, so read them here rather than assuming.${string}`;
|
|
40
|
+
readonly annotations: {
|
|
41
|
+
readonly title: string;
|
|
42
|
+
readonly readOnlyHint: true;
|
|
43
|
+
readonly destructiveHint: false;
|
|
44
|
+
readonly openWorldHint: false;
|
|
45
|
+
};
|
|
46
|
+
readonly inputSchema: {
|
|
47
|
+
readonly type: "object";
|
|
48
|
+
readonly properties: {
|
|
49
|
+
readonly product: {
|
|
50
|
+
readonly type: "string";
|
|
51
|
+
readonly description: `The ${string} name or handle${string}`;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
readonly required: readonly ["product"];
|
|
55
|
+
};
|
|
56
|
+
}, {
|
|
57
|
+
readonly name: string;
|
|
58
|
+
readonly description: `Get the checkout link for ONE specific ${string} variant. Fill EVERY option the product has \u2014 if it offers two, both are required, and they are usually different. Never assume one answer carries to another option, and never round to a nearby value: ask. If the choices do not name exactly one variant this returns the options to ask about instead of guessing.${string}`;
|
|
59
|
+
readonly annotations: {
|
|
60
|
+
readonly title: string;
|
|
61
|
+
readonly readOnlyHint: true;
|
|
62
|
+
readonly destructiveHint: false;
|
|
63
|
+
readonly openWorldHint: false;
|
|
64
|
+
};
|
|
65
|
+
readonly inputSchema: {
|
|
66
|
+
readonly type: "object";
|
|
67
|
+
readonly properties: {
|
|
68
|
+
readonly product: {
|
|
69
|
+
readonly type: "string";
|
|
70
|
+
readonly description: `The ${string} name or handle`;
|
|
71
|
+
};
|
|
72
|
+
readonly options: {
|
|
73
|
+
readonly type: "object";
|
|
74
|
+
readonly description: `One value per option, keyed by the store's own option names.${string}`;
|
|
75
|
+
readonly additionalProperties: {
|
|
76
|
+
readonly type: "string";
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
readonly quantity: {
|
|
80
|
+
readonly type: "number";
|
|
81
|
+
readonly description: "Default 1";
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
readonly required: readonly ["product", "options"];
|
|
85
|
+
};
|
|
86
|
+
}, {
|
|
87
|
+
readonly name: string;
|
|
88
|
+
readonly description: "Find an order the buyer already placed. BOTH the order number and the email it was placed with are required — that pair is what proves the order is theirs.";
|
|
89
|
+
readonly annotations: {
|
|
90
|
+
readonly title: string;
|
|
91
|
+
readonly readOnlyHint: true;
|
|
92
|
+
readonly destructiveHint: false;
|
|
93
|
+
readonly openWorldHint: false;
|
|
94
|
+
};
|
|
95
|
+
readonly inputSchema: {
|
|
96
|
+
readonly type: "object";
|
|
97
|
+
readonly properties: {
|
|
98
|
+
readonly orderNumber: {
|
|
99
|
+
readonly type: "string";
|
|
100
|
+
readonly description: "From the confirmation email, e.g. \"#1052\"";
|
|
101
|
+
};
|
|
102
|
+
readonly email: {
|
|
103
|
+
readonly type: "string";
|
|
104
|
+
readonly description: "The email the order was placed with";
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
readonly required: readonly ["orderNumber", "email"];
|
|
108
|
+
};
|
|
109
|
+
}];
|
|
110
|
+
export declare const buildOutputSchemas: (n: ToolNames) => Record<string, unknown>;
|
|
111
|
+
/** One choice in a filter row. `tags` is what a product must carry to match — a
|
|
112
|
+
* list because a bucket like "Large" covers several diameters. */
|
|
113
|
+
export type FacetValue = {
|
|
114
|
+
label: string;
|
|
115
|
+
tags: string[];
|
|
116
|
+
count: number;
|
|
117
|
+
};
|
|
118
|
+
export type Facet = {
|
|
119
|
+
axis: string;
|
|
120
|
+
values: FacetValue[];
|
|
121
|
+
};
|
|
122
|
+
export declare const isAccessory: (title: string) => boolean;
|
|
123
|
+
export type ToolDeps = {
|
|
124
|
+
catalog: Catalog;
|
|
125
|
+
/** This storefront's tool names — the dispatch below compares against these, not literals. */
|
|
126
|
+
tools: ToolNames;
|
|
127
|
+
/** The words this shop's goods are called, for the card's own copy. */
|
|
128
|
+
vocab: {
|
|
129
|
+
noun: string;
|
|
130
|
+
nounPlural: string;
|
|
131
|
+
};
|
|
132
|
+
shopify: ShopifyConfig;
|
|
133
|
+
/** Shown to the buyer as the merchant's name. */
|
|
134
|
+
merchantName: string;
|
|
135
|
+
/**
|
|
136
|
+
* THIS merchant's wordmark, for the card header.
|
|
137
|
+
*
|
|
138
|
+
* The card used to carry one baked into its HTML — which was the first
|
|
139
|
+
* merchant's, so every later storefront wore their brand. Passed in, and
|
|
140
|
+
* omitted when the merchant supplied none: a header with no wordmark falls
|
|
141
|
+
* back to the shop's name, which is at least true.
|
|
142
|
+
*/
|
|
143
|
+
logoUrl?: string;
|
|
144
|
+
/**
|
|
145
|
+
* Where the card should ask for product images — our own `/img?u=` in front of
|
|
146
|
+
* Shopify's CDN. The card is drawn in a sandboxed iframe whose CSP the host
|
|
147
|
+
* sets, and Claude blocks cdn.shopify.com outright; the connector's own origin
|
|
148
|
+
* is the one host such an iframe is plausibly allowed to load from. The
|
|
149
|
+
* original CDN url rides along as `imageCdn` so the card can fall back.
|
|
150
|
+
*/
|
|
151
|
+
imageProxy?: string;
|
|
152
|
+
/**
|
|
153
|
+
* Which assistant is shopping, when the client told us at handshake
|
|
154
|
+
* ("claude-ai", "ChatGPT", …). It rides along on the cart as an attribute so
|
|
155
|
+
* the merchant can see where an order came from — the order's `Channel` will
|
|
156
|
+
* always say Online Store, because that is the channel that created it.
|
|
157
|
+
*/
|
|
158
|
+
assistant?: string;
|
|
159
|
+
};
|
|
160
|
+
export type ToolResult = {
|
|
161
|
+
structured: Record<string, unknown>;
|
|
162
|
+
text: string;
|
|
163
|
+
};
|
|
164
|
+
export declare const callTool: (deps: ToolDeps, name: string, args: Record<string, unknown>) => Promise<ToolResult>;
|
|
165
|
+
/**
|
|
166
|
+
* `productSummary` under test. Exported rather than reached through a live
|
|
167
|
+
* catalogue because the thing worth pinning is the SHAPE — which fields exist
|
|
168
|
+
* for which store — and a fetch mock would bury that behind plumbing.
|
|
169
|
+
*/
|
|
170
|
+
export declare const productSummaryForTest: (p: ShopifyProduct, imageProxy?: string) => {
|
|
171
|
+
type: string | undefined;
|
|
172
|
+
tags: string[];
|
|
173
|
+
image: string | undefined;
|
|
174
|
+
imageCdn: string | undefined;
|
|
175
|
+
eyes: "unknown" | "pair" | "single";
|
|
176
|
+
prescriptions: string[];
|
|
177
|
+
name: string;
|
|
178
|
+
handle: string;
|
|
179
|
+
url: string | undefined;
|
|
180
|
+
price: string | undefined;
|
|
181
|
+
choices: {
|
|
182
|
+
name: string;
|
|
183
|
+
count: number;
|
|
184
|
+
}[];
|
|
185
|
+
} | {
|
|
186
|
+
type: string | undefined;
|
|
187
|
+
tags: string[];
|
|
188
|
+
image: string | undefined;
|
|
189
|
+
imageCdn: string | undefined;
|
|
190
|
+
eyes?: undefined;
|
|
191
|
+
prescriptions?: undefined;
|
|
192
|
+
name: string;
|
|
193
|
+
handle: string;
|
|
194
|
+
url: string | undefined;
|
|
195
|
+
price: string | undefined;
|
|
196
|
+
choices: {
|
|
197
|
+
name: string;
|
|
198
|
+
count: number;
|
|
199
|
+
}[];
|
|
200
|
+
};
|
|
201
|
+
//# sourceMappingURL=tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,cAAc,EAKpB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAO5C,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAazD;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,GAAI,GAAG,SAAS,EAAE,GAAG,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoF1C,CAAC;AAkBb,eAAO,MAAM,kBAAkB,GAAI,GAAG,SAAS,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAwEtE,CAAC;AA6EH;kEACkE;AAClE,MAAM,MAAM,UAAU,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAC1E,MAAM,MAAM,KAAK,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,UAAU,EAAE,CAAA;CAAE,CAAC;AA8I3D,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,KAAG,OAAgC,CAAC;AAiC7E,MAAM,MAAM,QAAQ,GAAG;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,8FAA8F;IAC9F,KAAK,EAAE,SAAS,CAAC;IACjB,uEAAuE;IACvE,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,OAAO,EAAE,aAAa,CAAC;IACvB,iDAAiD;IACjD,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAW/E,eAAO,MAAM,QAAQ,GACnB,MAAM,QAAQ,EACd,MAAM,MAAM,EACZ,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC5B,OAAO,CAAC,UAAU,CAqWpB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,GAAI,GAAG,cAAc,EAAE,aAAa,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAC7C,CAAC"}
|