@businessdash/sdk 0.9.60 → 0.9.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1012 -186
- package/assets/biab-sw.js +92 -0
- package/dist/account-core/index.d.ts +126 -0
- package/dist/account-core/index.d.ts.map +1 -0
- package/dist/account-core/index.js +191 -0
- package/dist/account-core/index.js.map +1 -0
- package/dist/account.d.ts +11 -0
- package/dist/account.d.ts.map +1 -0
- package/dist/account.js +11 -0
- package/dist/account.js.map +1 -0
- package/dist/auth-handler.d.ts +1 -1
- package/dist/auth-links.d.ts +33 -0
- package/dist/auth-links.d.ts.map +1 -0
- package/dist/auth-links.js +64 -0
- package/dist/auth-links.js.map +1 -0
- package/dist/blog-core/index.d.ts +112 -0
- package/dist/blog-core/index.d.ts.map +1 -0
- package/dist/blog-core/index.js +330 -0
- package/dist/blog-core/index.js.map +1 -0
- package/dist/blog.d.ts +11 -0
- package/dist/blog.d.ts.map +1 -0
- package/dist/blog.js +11 -0
- package/dist/blog.js.map +1 -0
- package/dist/chat-core/index.d.ts +97 -0
- package/dist/chat-core/index.d.ts.map +1 -0
- package/dist/chat-core/index.js +237 -0
- package/dist/chat-core/index.js.map +1 -0
- package/dist/chat.d.ts +11 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +11 -0
- package/dist/chat.js.map +1 -0
- package/dist/checkout-core/index.d.ts +65 -0
- package/dist/checkout-core/index.d.ts.map +1 -0
- package/dist/checkout-core/index.js +159 -0
- package/dist/checkout-core/index.js.map +1 -0
- package/dist/checkout.d.ts +10 -0
- package/dist/checkout.d.ts.map +1 -0
- package/dist/checkout.js +10 -0
- package/dist/checkout.js.map +1 -0
- package/dist/cli.js +133 -25
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +567 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +807 -1
- package/dist/client.js.map +1 -1
- package/dist/contracts.d.ts +3309 -761
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +541 -4
- package/dist/contracts.js.map +1 -1
- package/dist/data-model-schema.d.ts +1 -1
- package/dist/data-model-schema.d.ts.map +1 -1
- package/dist/element.d.ts +30 -0
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +56 -0
- package/dist/element.js.map +1 -1
- package/dist/embed-protocol.d.ts +86 -0
- package/dist/embed-protocol.d.ts.map +1 -0
- package/dist/embed-protocol.js +60 -0
- package/dist/embed-protocol.js.map +1 -0
- package/dist/framework-support.d.ts +122 -0
- package/dist/framework-support.d.ts.map +1 -0
- package/dist/framework-support.js +323 -0
- package/dist/framework-support.js.map +1 -0
- package/dist/index.cjs +1244 -5
- package/dist/legal-core/index.cjs +115 -0
- package/dist/legal-core/index.d.ts +160 -0
- package/dist/legal-core/index.d.ts.map +1 -0
- package/dist/legal-core/index.js +220 -0
- package/dist/legal-core/index.js.map +1 -0
- package/dist/marketing-core/index.d.ts +66 -0
- package/dist/marketing-core/index.d.ts.map +1 -0
- package/dist/marketing-core/index.js +154 -0
- package/dist/marketing-core/index.js.map +1 -0
- package/dist/marketing.d.ts +7 -0
- package/dist/marketing.d.ts.map +1 -0
- package/dist/marketing.js +7 -0
- package/dist/marketing.js.map +1 -0
- package/dist/mcp-adapters/index.d.ts +135 -0
- package/dist/mcp-adapters/index.d.ts.map +1 -0
- package/dist/mcp-adapters/index.js +187 -0
- package/dist/mcp-adapters/index.js.map +1 -0
- package/dist/mcp-adapters.d.ts +7 -0
- package/dist/mcp-adapters.d.ts.map +1 -0
- package/dist/mcp-adapters.js +7 -0
- package/dist/mcp-adapters.js.map +1 -0
- package/dist/model3d-core/index.cjs +95 -0
- package/dist/model3d-core/index.d.ts +167 -0
- package/dist/model3d-core/index.d.ts.map +1 -0
- package/dist/model3d-core/index.js +158 -0
- package/dist/model3d-core/index.js.map +1 -0
- package/dist/notifications-core/index.cjs +315 -0
- package/dist/notifications-core/index.d.ts +229 -0
- package/dist/notifications-core/index.d.ts.map +1 -0
- package/dist/notifications-core/index.js +445 -0
- package/dist/notifications-core/index.js.map +1 -0
- package/dist/portal-core/index.d.ts +451 -0
- package/dist/portal-core/index.d.ts.map +1 -0
- package/dist/portal-core/index.js +819 -0
- package/dist/portal-core/index.js.map +1 -0
- package/dist/portal.d.ts +7 -0
- package/dist/portal.d.ts.map +1 -0
- package/dist/portal.js +7 -0
- package/dist/portal.js.map +1 -0
- package/dist/push-core/index.cjs +171 -0
- package/dist/push-core/index.d.ts +149 -0
- package/dist/push-core/index.d.ts.map +1 -0
- package/dist/push-core/index.js +231 -0
- package/dist/push-core/index.js.map +1 -0
- package/dist/react-marketing.d.ts +1 -17
- package/dist/react-marketing.d.ts.map +1 -1
- package/dist/react-marketing.js +4 -0
- package/dist/react-marketing.js.map +1 -1
- package/dist/react.d.ts +294 -101
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +302 -246
- package/dist/react.js.map +1 -1
- package/dist/seed-core/index.d.ts +354 -0
- package/dist/seed-core/index.d.ts.map +1 -0
- package/dist/seed-core/index.js +420 -0
- package/dist/seed-core/index.js.map +1 -0
- package/dist/seed.d.ts +7 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +7 -0
- package/dist/seed.js.map +1 -0
- package/dist/seo-core/blog.d.ts +94 -0
- package/dist/seo-core/blog.d.ts.map +1 -0
- package/dist/seo-core/blog.js +149 -0
- package/dist/seo-core/blog.js.map +1 -0
- package/dist/seo-core/commerce.d.ts +64 -0
- package/dist/seo-core/commerce.d.ts.map +1 -0
- package/dist/seo-core/commerce.js +87 -0
- package/dist/seo-core/commerce.js.map +1 -0
- package/dist/seo-core/index.cjs +751 -0
- package/dist/seo-core/index.d.ts +226 -0
- package/dist/seo-core/index.d.ts.map +1 -0
- package/dist/seo-core/index.js +455 -0
- package/dist/seo-core/index.js.map +1 -0
- package/dist/seo-core/local.d.ts +181 -0
- package/dist/seo-core/local.d.ts.map +1 -0
- package/dist/seo-core/local.js +193 -0
- package/dist/seo-core/local.js.map +1 -0
- package/dist/sitemap-core/index.cjs +167 -0
- package/dist/sitemap-core/index.d.ts +155 -0
- package/dist/sitemap-core/index.d.ts.map +1 -0
- package/dist/sitemap-core/index.js +195 -0
- package/dist/sitemap-core/index.js.map +1 -0
- package/dist/social-core/index.d.ts +74 -0
- package/dist/social-core/index.d.ts.map +1 -0
- package/dist/social-core/index.js +72 -0
- package/dist/social-core/index.js.map +1 -0
- package/dist/social.d.ts +7 -0
- package/dist/social.d.ts.map +1 -0
- package/dist/social.js +7 -0
- package/dist/social.js.map +1 -0
- package/dist/solid-compat.d.ts +204 -0
- package/dist/solid-compat.d.ts.map +1 -0
- package/dist/solid-compat.js +221 -0
- package/dist/solid-compat.js.map +1 -0
- package/dist/store-core/index.d.ts +141 -0
- package/dist/store-core/index.d.ts.map +1 -0
- package/dist/store-core/index.js +277 -0
- package/dist/store-core/index.js.map +1 -0
- package/dist/store.d.ts +12 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +12 -0
- package/dist/store.js.map +1 -0
- package/dist/vanilla.d.ts +111 -0
- package/dist/vanilla.d.ts.map +1 -1
- package/dist/vanilla.js +206 -0
- package/dist/vanilla.js.map +1 -1
- package/dist/verify-core/index.d.ts +98 -0
- package/dist/verify-core/index.d.ts.map +1 -0
- package/dist/verify-core/index.js +186 -0
- package/dist/verify-core/index.js.map +1 -0
- package/dist/verify.d.ts +8 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +8 -0
- package/dist/verify.js.map +1 -0
- package/openapi.json +4316 -1313
- package/package.json +121 -9
- package/src/account-core/index.ts +309 -0
- package/src/account.ts +11 -0
- package/src/angular/headless.ts +519 -0
- package/src/angular/index.ts +3 -0
- package/src/auth-links.ts +76 -0
- package/src/blog-core/index.ts +423 -0
- package/src/blog.ts +11 -0
- package/src/chat-core/index.ts +335 -0
- package/src/chat.ts +11 -0
- package/src/checkout-core/index.ts +225 -0
- package/src/checkout.ts +10 -0
- package/src/cli.ts +184 -28
- package/src/client.ts +998 -0
- package/src/contracts.ts +730 -11
- package/src/data-model-schema.ts +1 -1
- package/src/element.ts +102 -0
- package/src/embed-protocol.ts +100 -0
- package/src/framework-support.ts +440 -0
- package/src/legal-core/index.ts +276 -0
- package/src/marketing-core/index.ts +209 -0
- package/src/marketing.ts +7 -0
- package/src/mcp-adapters/index.ts +264 -0
- package/src/mcp-adapters.ts +7 -0
- package/src/model3d-core/index.ts +248 -0
- package/src/notifications-core/index.ts +650 -0
- package/src/portal-core/index.ts +1564 -0
- package/src/portal.ts +7 -0
- package/src/push-core/index.ts +335 -0
- package/src/qwik.tsx +602 -2
- package/src/react-marketing.tsx +5 -0
- package/src/react.tsx +434 -319
- package/src/seed-core/index.ts +735 -0
- package/src/seed.ts +7 -0
- package/src/seo-core/blog.ts +216 -0
- package/src/seo-core/commerce.ts +138 -0
- package/src/seo-core/index.ts +605 -0
- package/src/seo-core/local.ts +333 -0
- package/src/sitemap-core/index.ts +296 -0
- package/src/social-core/index.ts +103 -0
- package/src/social.ts +7 -0
- package/src/solid-compat.tsx +347 -0
- package/src/solid-surfaces.tsx +1311 -0
- package/src/solid.tsx +82 -60
- package/src/store-core/index.ts +426 -0
- package/src/store.ts +12 -0
- package/src/svelte/SocialLinks.svelte +84 -0
- package/src/svelte/headless.ts +435 -0
- package/src/svelte/index.ts +9 -0
- package/src/vanilla.ts +367 -0
- package/src/verify-core/index.ts +288 -0
- package/src/verify.ts +8 -0
- package/src/vue/headless.ts +527 -0
- package/src/vue/index.ts +3 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@businessdash/sdk/mcp-adapters` — mount the MCP connector on any framework.
|
|
3
|
+
*
|
|
4
|
+
* `mcpHandler` returns Web-standard `(Request) => Promise<Response>`, which is
|
|
5
|
+
* exactly right for Next's App Router, Astro, Remix and TanStack Start, and
|
|
6
|
+
* exactly wrong for everything else: SvelteKit hands you `{ request }`,
|
|
7
|
+
* Nuxt/Nitro hands you an H3 `event`, Qwik City hands you a `RequestEvent` with
|
|
8
|
+
* `send()`, and Express hands you Node's `req`/`res`.
|
|
9
|
+
*
|
|
10
|
+
* So a consumer on four of the eight frameworks had to work out the bridging
|
|
11
|
+
* themselves, for a surface whose whole selling point is that an org hands
|
|
12
|
+
* Claude or ChatGPT a URL on their OWN domain. This module is that bridging,
|
|
13
|
+
* written once.
|
|
14
|
+
*
|
|
15
|
+
* Every adapter is a thin shape-change over the same handler — no adapter
|
|
16
|
+
* reimplements the proxy, the error mapping or the manifest caching, so a fix
|
|
17
|
+
* to any of those lands everywhere at once.
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
type McpProxyOptions,
|
|
24
|
+
mcpHandler,
|
|
25
|
+
mcpManifestHandler,
|
|
26
|
+
} from "../mcp.js";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* SvelteKit (and Astro, which has the same shape).
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* // src/routes/api/mcp/+server.ts
|
|
33
|
+
* import { sveltekitMcpHandler } from '@businessdash/sdk/mcp-adapters'
|
|
34
|
+
* export const { POST, GET } = sveltekitMcpHandler({ siteId, baseUrl })
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function sveltekitMcpHandler(options: McpProxyOptions): {
|
|
38
|
+
POST: (event: { request: Request }) => Promise<Response>;
|
|
39
|
+
GET: () => Promise<Response>;
|
|
40
|
+
} {
|
|
41
|
+
const handler = mcpHandler(options);
|
|
42
|
+
return {
|
|
43
|
+
POST: (event) => handler.POST(event.request),
|
|
44
|
+
GET: () => handler.GET(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** SvelteKit / Astro manifest route for `.well-known/mcp.json`. */
|
|
49
|
+
export function sveltekitMcpManifestHandler(
|
|
50
|
+
options: McpProxyOptions,
|
|
51
|
+
): (event: { request: Request }) => Promise<Response> {
|
|
52
|
+
const handler = mcpManifestHandler(options);
|
|
53
|
+
return (event) => handler(event.request);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Astro is the same shape as SvelteKit; aliased so the import reads right. */
|
|
57
|
+
export const astroMcpHandler = sveltekitMcpHandler;
|
|
58
|
+
export const astroMcpManifestHandler = sveltekitMcpManifestHandler;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The minimum of H3's event we touch. Declared structurally rather than
|
|
62
|
+
* importing `h3`, so this module adds no dependency for consumers who are not
|
|
63
|
+
* on Nuxt.
|
|
64
|
+
*/
|
|
65
|
+
type H3EventLike = {
|
|
66
|
+
node: { req: { method?: string | undefined } };
|
|
67
|
+
web?: { request?: Request } | undefined;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Nuxt / Nitro.
|
|
72
|
+
*
|
|
73
|
+
* ```ts
|
|
74
|
+
* // server/routes/api/mcp.post.ts
|
|
75
|
+
* import { nitroMcpHandler } from '@businessdash/sdk/mcp-adapters'
|
|
76
|
+
* export default defineEventHandler(nitroMcpHandler({ siteId, baseUrl }))
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* Nitro exposes the original Web `Request` on `event.web.request` in recent
|
|
80
|
+
* versions. `toWebRequest` is passed in rather than imported so this stays
|
|
81
|
+
* dependency-free — on older Nitro, hand it `toWebRequest` from `h3`.
|
|
82
|
+
*/
|
|
83
|
+
export function nitroMcpHandler(
|
|
84
|
+
options: McpProxyOptions,
|
|
85
|
+
deps: { toWebRequest?: (event: unknown) => Request } = {},
|
|
86
|
+
): (event: H3EventLike) => Promise<Response> {
|
|
87
|
+
const handler = mcpHandler(options);
|
|
88
|
+
return async (event) => {
|
|
89
|
+
const request = event.web?.request ?? deps.toWebRequest?.(event);
|
|
90
|
+
if (!request) {
|
|
91
|
+
// Failing loudly beats proxying an empty body and returning a
|
|
92
|
+
// confusing JSON-RPC error the consumer cannot trace.
|
|
93
|
+
throw new Error(
|
|
94
|
+
"nitroMcpHandler could not read a Web Request from the H3 event. On Nitro versions without `event.web.request`, pass `{ toWebRequest }` from `h3`.",
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return (event.node.req.method ?? "GET").toUpperCase() === "POST"
|
|
98
|
+
? handler.POST(request)
|
|
99
|
+
: handler.GET();
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Nuxt / Nitro manifest route. */
|
|
104
|
+
export function nitroMcpManifestHandler(
|
|
105
|
+
options: McpProxyOptions,
|
|
106
|
+
deps: { toWebRequest?: (event: unknown) => Request } = {},
|
|
107
|
+
): (event: H3EventLike) => Promise<Response> {
|
|
108
|
+
const handler = mcpManifestHandler(options);
|
|
109
|
+
return async (event) => {
|
|
110
|
+
const request = event.web?.request ?? deps.toWebRequest?.(event);
|
|
111
|
+
if (!request) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
"nitroMcpManifestHandler could not read a Web Request from the H3 event. Pass `{ toWebRequest }` from `h3` on older Nitro.",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return handler(request);
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The slice of Qwik City's `RequestEvent` these adapters use. */
|
|
121
|
+
type QwikRequestEventLike = {
|
|
122
|
+
request: Request;
|
|
123
|
+
send: (response: Response) => void;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Qwik City.
|
|
128
|
+
*
|
|
129
|
+
* ```ts
|
|
130
|
+
* // src/routes/api/mcp/index.ts
|
|
131
|
+
* import { qwikMcpHandler } from '@businessdash/sdk/mcp-adapters'
|
|
132
|
+
* const mcp = qwikMcpHandler({ siteId, baseUrl })
|
|
133
|
+
* export const onPost = mcp.onPost
|
|
134
|
+
* export const onGet = mcp.onGet
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* Qwik City returns a response by CALLING `send()` rather than returning it,
|
|
138
|
+
* which is the detail most people get wrong when wiring this by hand.
|
|
139
|
+
*/
|
|
140
|
+
export function qwikMcpHandler(options: McpProxyOptions): {
|
|
141
|
+
onPost: (event: QwikRequestEventLike) => Promise<void>;
|
|
142
|
+
onGet: (event: QwikRequestEventLike) => Promise<void>;
|
|
143
|
+
} {
|
|
144
|
+
const handler = mcpHandler(options);
|
|
145
|
+
return {
|
|
146
|
+
onPost: async (event) => {
|
|
147
|
+
event.send(await handler.POST(event.request));
|
|
148
|
+
},
|
|
149
|
+
onGet: async (event) => {
|
|
150
|
+
event.send(await handler.GET());
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Qwik City manifest route. */
|
|
156
|
+
export function qwikMcpManifestHandler(
|
|
157
|
+
options: McpProxyOptions,
|
|
158
|
+
): (event: QwikRequestEventLike) => Promise<void> {
|
|
159
|
+
const handler = mcpManifestHandler(options);
|
|
160
|
+
return async (event) => {
|
|
161
|
+
event.send(await handler(event.request));
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** The Node req/res surface these adapters touch. */
|
|
166
|
+
type NodeRequestLike = {
|
|
167
|
+
method?: string | undefined;
|
|
168
|
+
url?: string | undefined;
|
|
169
|
+
headers: Record<string, string | string[] | undefined>;
|
|
170
|
+
on(event: string, listener: (chunk?: unknown) => void): unknown;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
type NodeResponseLike = {
|
|
174
|
+
statusCode: number;
|
|
175
|
+
setHeader(name: string, value: string): void;
|
|
176
|
+
end(body?: string): void;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Express, and Angular Universal / Analog, which both run on Node's req/res.
|
|
181
|
+
*
|
|
182
|
+
* ```ts
|
|
183
|
+
* const mcp = expressMcpHandler({ siteId, baseUrl })
|
|
184
|
+
* app.post('/api/mcp', mcp)
|
|
185
|
+
* app.get('/api/mcp', mcp)
|
|
186
|
+
* app.get('/.well-known/mcp.json', expressMcpManifestHandler({ siteId, baseUrl }))
|
|
187
|
+
* ```
|
|
188
|
+
*
|
|
189
|
+
* Node gives a stream rather than a `Request`, so the body is collected first.
|
|
190
|
+
* `origin` matters for the manifest — it is what tells the platform which
|
|
191
|
+
* domain to advertise — so it is reconstructed from the Host header rather
|
|
192
|
+
* than guessed.
|
|
193
|
+
*/
|
|
194
|
+
export function expressMcpHandler(
|
|
195
|
+
options: McpProxyOptions,
|
|
196
|
+
): (req: NodeRequestLike, res: NodeResponseLike) => Promise<void> {
|
|
197
|
+
const handler = mcpHandler(options);
|
|
198
|
+
return async (req, res) => {
|
|
199
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
200
|
+
const response =
|
|
201
|
+
method === "POST"
|
|
202
|
+
? await handler.POST(
|
|
203
|
+
new Request(reconstructUrl(req), {
|
|
204
|
+
method: "POST",
|
|
205
|
+
headers: {
|
|
206
|
+
"content-type": "application/json",
|
|
207
|
+
accept: headerValue(req.headers.accept) ?? "application/json",
|
|
208
|
+
},
|
|
209
|
+
body: await readBody(req),
|
|
210
|
+
}),
|
|
211
|
+
)
|
|
212
|
+
: await handler.GET();
|
|
213
|
+
await writeNodeResponse(res, response);
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Express / Node manifest route for `.well-known/mcp.json`. */
|
|
218
|
+
export function expressMcpManifestHandler(
|
|
219
|
+
options: McpProxyOptions,
|
|
220
|
+
): (req: NodeRequestLike, res: NodeResponseLike) => Promise<void> {
|
|
221
|
+
const handler = mcpManifestHandler(options);
|
|
222
|
+
return async (req, res) => {
|
|
223
|
+
const response = await handler(new Request(reconstructUrl(req)));
|
|
224
|
+
await writeNodeResponse(res, response);
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function headerValue(value: string | string[] | undefined): string | undefined {
|
|
229
|
+
return Array.isArray(value) ? value[0] : value;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Rebuild the absolute URL Node does not give us.
|
|
234
|
+
*
|
|
235
|
+
* The manifest handler reads `new URL(request.url).origin` to tell the platform
|
|
236
|
+
* which domain to advertise, so a placeholder host here would publish the wrong
|
|
237
|
+
* connector URL — the one failure mode of this whole surface.
|
|
238
|
+
*/
|
|
239
|
+
function reconstructUrl(req: NodeRequestLike): string {
|
|
240
|
+
const host = headerValue(req.headers.host) ?? "localhost";
|
|
241
|
+
const proto = headerValue(req.headers["x-forwarded-proto"]) ?? "https";
|
|
242
|
+
return `${proto}://${host}${req.url ?? "/"}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function readBody(req: NodeRequestLike): Promise<string> {
|
|
246
|
+
return new Promise((resolve, reject) => {
|
|
247
|
+
const chunks: string[] = [];
|
|
248
|
+
req.on("data", (chunk) => chunks.push(String(chunk)));
|
|
249
|
+
req.on("end", () => resolve(chunks.join("")));
|
|
250
|
+
req.on("error", (error) => reject(error));
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function writeNodeResponse(
|
|
255
|
+
res: NodeResponseLike,
|
|
256
|
+
response: Response,
|
|
257
|
+
): Promise<void> {
|
|
258
|
+
res.statusCode = response.status;
|
|
259
|
+
response.headers.forEach((value, name) => {
|
|
260
|
+
res.setHeader(name, value);
|
|
261
|
+
});
|
|
262
|
+
const text = await response.text();
|
|
263
|
+
res.end(text.length > 0 ? text : undefined);
|
|
264
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@businessdash/sdk/model3d` — 3D product models on the org's own site.
|
|
3
|
+
*
|
|
4
|
+
* ## This ships no renderer, and that is the point
|
|
5
|
+
*
|
|
6
|
+
* A three.js build is ~600KB before your model. `<model-viewer>` is ~300KB. An
|
|
7
|
+
* SDK that pulled either down on every product page would be an SDK people rip
|
|
8
|
+
* out, and it would make the choice of engine ours rather than yours.
|
|
9
|
+
*
|
|
10
|
+
* So this decides WHAT to show and hands you the facts. You mount whichever
|
|
11
|
+
* viewer you already use, or none.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* const plan = planProductModel(product);
|
|
15
|
+
* if (plan.ar) showArLink(plan.ar.url); // zero JS, native iOS
|
|
16
|
+
* if (plan.viewer) mountYourViewer(plan.viewer.url, plan.viewer.format);
|
|
17
|
+
* if (plan.download) showLink(plan.download.url, plan.download.label);
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* ## The three formats do genuinely different jobs
|
|
21
|
+
*
|
|
22
|
+
* **AR Quick Look (`.usdz`)** — the one worth having. On iOS, iPadOS and
|
|
23
|
+
* visionOS Safari an `<a rel="ar">` opens the model in the room, in the camera,
|
|
24
|
+
* with no JavaScript at all. No library, no canvas, no WebGL. Where it applies
|
|
25
|
+
* it beats an embedded viewer on every axis, so it is checked first.
|
|
26
|
+
*
|
|
27
|
+
* **Web viewer (`.glb`/`.gltf`/`.obj`/`.stl`)** — the rotatable canvas. `.glb`
|
|
28
|
+
* carries its own materials and is what you want; `.obj` and `.stl` are
|
|
29
|
+
* geometry only and render in a neutral grey, which is fine for a machined part
|
|
30
|
+
* and disappointing for a sofa.
|
|
31
|
+
*
|
|
32
|
+
* **CAD download (`.step`/`.stp`/`.iges`)** — NOT renderable, deliberately.
|
|
33
|
+
* STEP is a boundary-representation format needing a geometry kernel; the
|
|
34
|
+
* browser WASM builds of OpenCascade that can tessellate it are 10–30MB, which
|
|
35
|
+
* is not something to pull down for a product photo. It is also the wrong shape
|
|
36
|
+
* for who asks: an engineer evaluating a bracket wants the file in their own CAD
|
|
37
|
+
* package, not a spinning preview.
|
|
38
|
+
*
|
|
39
|
+
* ## Absent is the normal case
|
|
40
|
+
*
|
|
41
|
+
* These fields are stripped unless the org holds the `ecommerce.product_3d`
|
|
42
|
+
* add-on, and most businesses never attach a model. Every function here treats
|
|
43
|
+
* missing as ordinary and returns nothing to render.
|
|
44
|
+
*
|
|
45
|
+
* @module
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/** The product fields this reads. Anything with these keys works. */
|
|
49
|
+
export type ModelBearingProduct = {
|
|
50
|
+
id?: string;
|
|
51
|
+
model3dUrl?: string | null;
|
|
52
|
+
model3dUsdzUrl?: string | null;
|
|
53
|
+
/** The API reports availability, never the URL. See `download` below. */
|
|
54
|
+
model3dCadAvailable?: boolean;
|
|
55
|
+
model3dCadName?: string | null;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** What a web viewer can load. */
|
|
59
|
+
export type WebModelFormat = "glb" | "gltf" | "obj" | "stl";
|
|
60
|
+
|
|
61
|
+
/** What a CAD download might be. */
|
|
62
|
+
export type CadFormat = "step" | "iges" | "native";
|
|
63
|
+
|
|
64
|
+
export type ModelPlan = {
|
|
65
|
+
/**
|
|
66
|
+
* Present when the model can be placed in the room on this device.
|
|
67
|
+
*
|
|
68
|
+
* Only on Apple platforms, and only with a `.usdz`. Render as
|
|
69
|
+
* `<a rel="ar" href={url}><img …/></a>` — the `<img>` child is required by
|
|
70
|
+
* Quick Look, and the link needs no JavaScript whatsoever.
|
|
71
|
+
*/
|
|
72
|
+
ar: { url: string } | null;
|
|
73
|
+
/**
|
|
74
|
+
* Present when there is a model a WebGL viewer can load.
|
|
75
|
+
*
|
|
76
|
+
* `neutralMaterial` is true for `.obj` and `.stl`, which carry geometry but
|
|
77
|
+
* no materials — worth knowing, because a viewer that lights them like a
|
|
78
|
+
* `.glb` renders a black silhouette.
|
|
79
|
+
*/
|
|
80
|
+
viewer: {
|
|
81
|
+
url: string;
|
|
82
|
+
format: WebModelFormat;
|
|
83
|
+
neutralMaterial: boolean;
|
|
84
|
+
} | null;
|
|
85
|
+
/**
|
|
86
|
+
* Present when the org attached a CAD file.
|
|
87
|
+
*
|
|
88
|
+
* `url` points at the platform's gated endpoint, never at the file — that
|
|
89
|
+
* route requires cookies, rate-limits the caller and re-checks the
|
|
90
|
+
* entitlement before handing anything over. Render it as an ordinary link;
|
|
91
|
+
* the visitor's browser sends its cookie automatically.
|
|
92
|
+
*
|
|
93
|
+
* `format` is inferred from the filename when the org supplied one, so a
|
|
94
|
+
* page can say "STEP" rather than "CAD" without a round trip.
|
|
95
|
+
*/
|
|
96
|
+
download: { url: string; label: string; format: CadFormat } | null;
|
|
97
|
+
/** True when there is nothing 3D at all — the common case. */
|
|
98
|
+
empty: boolean;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** Lowercased extension, ignoring query strings and fragments. */
|
|
102
|
+
function extensionOf(url: string): string {
|
|
103
|
+
const path = url.split(/[?#]/)[0] ?? "";
|
|
104
|
+
const last = path.slice(path.lastIndexOf("/") + 1);
|
|
105
|
+
const dot = last.lastIndexOf(".");
|
|
106
|
+
return dot === -1 ? "" : last.slice(dot + 1).toLowerCase();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Which web viewer format a URL is, if any. */
|
|
110
|
+
export function webModelFormat(
|
|
111
|
+
url: string | null | undefined,
|
|
112
|
+
): WebModelFormat | null {
|
|
113
|
+
if (!url) return null;
|
|
114
|
+
const ext = extensionOf(url);
|
|
115
|
+
return ext === "glb" || ext === "gltf" || ext === "obj" || ext === "stl"
|
|
116
|
+
? ext
|
|
117
|
+
: null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Which CAD family a URL belongs to. Unknown extensions are `native`. */
|
|
121
|
+
export function cadFormat(url: string | null | undefined): CadFormat | null {
|
|
122
|
+
if (!url) return null;
|
|
123
|
+
const ext = extensionOf(url);
|
|
124
|
+
if (ext === "step" || ext === "stp") return "step";
|
|
125
|
+
if (ext === "iges" || ext === "igs") return "iges";
|
|
126
|
+
return ext ? "native" : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Does THIS browser support AR Quick Look?
|
|
131
|
+
*
|
|
132
|
+
* Feature-detected rather than sniffed: Safari advertises support by
|
|
133
|
+
* recognising `rel="ar"` on an anchor. That is Apple's own documented check and
|
|
134
|
+
* it survives the next user-agent change, which a regex on "iPhone" does not.
|
|
135
|
+
*
|
|
136
|
+
* False on the server, so a plan built during SSR omits the AR branch. Re-plan
|
|
137
|
+
* on the client if you want the link to appear without a navigation — the check
|
|
138
|
+
* is a few microseconds.
|
|
139
|
+
*/
|
|
140
|
+
export function supportsArQuickLook(): boolean {
|
|
141
|
+
if (typeof document === "undefined") return false;
|
|
142
|
+
const anchor = document.createElement("a");
|
|
143
|
+
return Boolean(anchor.relList?.supports?.("ar"));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export type PlanProductModelOptions = {
|
|
147
|
+
/**
|
|
148
|
+
* Override the AR capability check.
|
|
149
|
+
*
|
|
150
|
+
* Pass `true` during SSR when you know the request came from an Apple
|
|
151
|
+
* device, or `false` to suppress AR entirely.
|
|
152
|
+
*/
|
|
153
|
+
arSupported?: boolean;
|
|
154
|
+
/**
|
|
155
|
+
* Where your app proxies `GET storefront/products/{id}/cad`.
|
|
156
|
+
*
|
|
157
|
+
* Required for `download` to appear, and required to be YOUR route rather
|
|
158
|
+
* than the platform's: the API key is a server secret, so the browser
|
|
159
|
+
* cannot call the platform endpoint directly. Proxy it and pass your own
|
|
160
|
+
* path — `cadDownloadUrl: `/api/products/${product.id}/cad``.
|
|
161
|
+
*
|
|
162
|
+
* Without it the plan reports no download even when a file exists, which is
|
|
163
|
+
* the safe direction: a dead link is worse than an absent one.
|
|
164
|
+
*/
|
|
165
|
+
cadDownloadUrl?: string;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Decide what to render for one product.
|
|
170
|
+
*
|
|
171
|
+
* AR is not exclusive with the viewer — a product can offer both, and on an
|
|
172
|
+
* iPhone the usual arrangement is a canvas with a "View in your room" button
|
|
173
|
+
* over it. The plan reports each independently rather than picking for you.
|
|
174
|
+
*/
|
|
175
|
+
export function planProductModel(
|
|
176
|
+
product: ModelBearingProduct,
|
|
177
|
+
options: PlanProductModelOptions = {},
|
|
178
|
+
): ModelPlan {
|
|
179
|
+
const arSupported = options.arSupported ?? supportsArQuickLook();
|
|
180
|
+
const usdz = product.model3dUsdzUrl?.trim() || null;
|
|
181
|
+
const ar = arSupported && usdz ? { url: usdz } : null;
|
|
182
|
+
|
|
183
|
+
const viewerUrl = product.model3dUrl?.trim() || null;
|
|
184
|
+
const format = webModelFormat(viewerUrl);
|
|
185
|
+
const viewer =
|
|
186
|
+
viewerUrl && format
|
|
187
|
+
? {
|
|
188
|
+
url: viewerUrl,
|
|
189
|
+
format,
|
|
190
|
+
// .obj and .stl are geometry only. A viewer lighting them like
|
|
191
|
+
// a .glb renders a black silhouette.
|
|
192
|
+
neutralMaterial: format === "obj" || format === "stl",
|
|
193
|
+
}
|
|
194
|
+
: null;
|
|
195
|
+
|
|
196
|
+
const cadName = product.model3dCadName?.trim() || null;
|
|
197
|
+
// Inferred from the filename, since the URL is never in the payload. Falls
|
|
198
|
+
// back to `native`, which only changes the default label.
|
|
199
|
+
const cad = product.model3dCadAvailable
|
|
200
|
+
? (cadFormat(cadName) ?? "native")
|
|
201
|
+
: null;
|
|
202
|
+
const download =
|
|
203
|
+
cad && options.cadDownloadUrl
|
|
204
|
+
? {
|
|
205
|
+
url: options.cadDownloadUrl,
|
|
206
|
+
label: cadName || defaultCadLabel(cad),
|
|
207
|
+
format: cad,
|
|
208
|
+
}
|
|
209
|
+
: null;
|
|
210
|
+
|
|
211
|
+
return { ar, viewer, download, empty: !ar && !viewer && !download && !usdz };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function defaultCadLabel(format: CadFormat): string {
|
|
215
|
+
if (format === "step") return "Download STEP file";
|
|
216
|
+
if (format === "iges") return "Download IGES file";
|
|
217
|
+
return "Download CAD file";
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The attributes an AR Quick Look anchor needs.
|
|
222
|
+
*
|
|
223
|
+
* Spread onto your own `<a>`. Quick Look requires an `<img>` or equivalent
|
|
224
|
+
* child — an empty anchor silently does nothing, which is the single most
|
|
225
|
+
* common way this gets implemented wrong.
|
|
226
|
+
*/
|
|
227
|
+
export function arLinkAttributes(url: string): {
|
|
228
|
+
href: string;
|
|
229
|
+
rel: string;
|
|
230
|
+
} {
|
|
231
|
+
return { href: url, rel: "ar" };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* True when the org has the 3D add-on, judged from a product payload.
|
|
236
|
+
*
|
|
237
|
+
* The API omits these keys entirely without `ecommerce.product_3d`, so their
|
|
238
|
+
* absence distinguishes "not paid for" from "paid for, none attached" — where
|
|
239
|
+
* the key is present and null. Useful for deciding whether to show an upsell
|
|
240
|
+
* rather than nothing.
|
|
241
|
+
*/
|
|
242
|
+
export function hasModel3dEntitlement(product: object): boolean {
|
|
243
|
+
return (
|
|
244
|
+
"model3dUrl" in product ||
|
|
245
|
+
"model3dUsdzUrl" in product ||
|
|
246
|
+
"model3dCadAvailable" in product
|
|
247
|
+
);
|
|
248
|
+
}
|