@bacano/sdk 1.0.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,365 @@
1
+ # Storefront Buyer Authentication
2
+
3
+ This document is the canonical authentication contract for storefronts built
4
+ with `@bacano/sdk`. It is shipped with the package so a developer or coding
5
+ agent can integrate buyer accounts without access to the Bacano monorepo.
6
+
7
+ ## Ownership Boundary
8
+
9
+ Each Bacano website has its own Clerk application. This gives the same email
10
+ address an independent account and password in different storefronts.
11
+
12
+ Clerk owns:
13
+
14
+ - sign-up and sign-in;
15
+ - passwords and password recovery;
16
+ - email verification;
17
+ - session refresh, MFA, OAuth, and passkeys when enabled.
18
+
19
+ Bacano owns:
20
+
21
+ - mapping the verified Clerk user to a customer of the exact website;
22
+ - guest and authenticated carts;
23
+ - checkout and order ownership;
24
+ - customer profile and web order history;
25
+ - company and website isolation.
26
+
27
+ The storefront owns presentation only. It may use Clerk components or a custom
28
+ UI backed by Clerk's official SDK. Passwords and verification codes must go
29
+ directly from the browser to Clerk. They must never be sent to Bacano or
30
+ `@bacano/sdk`.
31
+
32
+ ## Security Invariants
33
+
34
+ - Never put a Clerk secret key, Bacano admin secret, or internal API key in the
35
+ browser.
36
+ - Never store the Clerk access token in `localStorage`. Clerk owns its session.
37
+ - Give Bacano a token provider function, not a captured token.
38
+ - Never send `clientId`, `companyId`, or a claimed buyer identity from checkout.
39
+ Bacano derives identity from the verified token.
40
+ - Never associate a guest purchase with an account only because emails match.
41
+ - Never query Hasura directly from a storefront.
42
+ - Always configure and send the exact Bacano `websiteSlug`.
43
+ - Always preserve the real storefront `Origin`, including protocol and port.
44
+
45
+ Bacano validates the explicit website slug, request `Origin`, token signature,
46
+ Clerk issuer, and the JWT `azp` authorized party before producing tenant-scoped
47
+ Hasura claims. The slug selects the website; Origin proves that the caller is
48
+ allowed to use it.
49
+
50
+ ## Public Website Bootstrap
51
+
52
+ The Clerk publishable key is website-specific and safe for browser use. Load it
53
+ at runtime instead of hardcoding one global Clerk application:
54
+
55
+ ```ts
56
+ import { getWebsitePublicConfig } from "@bacano/sdk";
57
+
58
+ const website = await getWebsitePublicConfig({
59
+ apiUrl: process.env.NEXT_PUBLIC_BACANO_API_URL!,
60
+ websiteSlug: process.env.NEXT_PUBLIC_BACANO_WEBSITE_SLUG!,
61
+ // Required for server-side calls. Browsers provide their own Origin.
62
+ origin: "https://store.example.com",
63
+ });
64
+
65
+ console.log(website.slug);
66
+ console.log(website.buyerAccountsEnabled);
67
+ console.log(website.clerkPublishableKey);
68
+ ```
69
+
70
+ The response exposes only browser-safe website data. Clerk secret keys and
71
+ JWKS configuration never leave Bacano.
72
+
73
+ If `buyerAccountsEnabled` is false or `clerkPublishableKey` is null, keep the
74
+ storefront in guest mode. Catalog, cart, and guest checkout remain available.
75
+
76
+ ## Next.js And React
77
+
78
+ Install the official Clerk integration used by the storefront framework. For
79
+ Next.js:
80
+
81
+ ```bash
82
+ pnpm add @clerk/nextjs
83
+ ```
84
+
85
+ Mount `BacanoProvider` inside the website-specific `ClerkProvider`:
86
+
87
+ ```tsx
88
+ "use client";
89
+
90
+ import { ClerkProvider, useAuth } from "@clerk/nextjs";
91
+ import { BacanoProvider } from "@bacano/sdk/react";
92
+ import type { ReactNode } from "react";
93
+
94
+ function StorefrontBacanoProvider({ children }: { children: ReactNode }) {
95
+ const { getToken } = useAuth();
96
+
97
+ return (
98
+ <BacanoProvider
99
+ config={{
100
+ apiUrl: process.env.NEXT_PUBLIC_BACANO_API_URL!,
101
+ websiteSlug: process.env.NEXT_PUBLIC_BACANO_WEBSITE_SLUG!,
102
+ getAccessToken: () => getToken(),
103
+ }}
104
+ >
105
+ {children}
106
+ </BacanoProvider>
107
+ );
108
+ }
109
+
110
+ export function StorefrontProviders({
111
+ publishableKey,
112
+ children,
113
+ }: {
114
+ publishableKey: string;
115
+ children: ReactNode;
116
+ }) {
117
+ return (
118
+ <ClerkProvider publishableKey={publishableKey}>
119
+ <StorefrontBacanoProvider>{children}</StorefrontBacanoProvider>
120
+ </ClerkProvider>
121
+ );
122
+ }
123
+ ```
124
+
125
+ Do not call `getToken()` once during initialization and retain its result. The
126
+ SDK calls `getAccessToken` for every request so Clerk can refresh or replace
127
+ the session token.
128
+
129
+ ## Framework-Agnostic Client
130
+
131
+ The same contract works without React:
132
+
133
+ ```ts
134
+ import { createBacanoClient } from "@bacano/sdk";
135
+
136
+ const client = createBacanoClient({
137
+ apiUrl: "https://api.bacanoerp.com",
138
+ websiteSlug: "my-store",
139
+ getAccessToken: async () => clerkSession?.getToken() ?? null,
140
+ });
141
+
142
+ await client.init();
143
+ ```
144
+
145
+ The SDK intentionally does not expose `login`, `register`, `logout`, or
146
+ password methods. Use the official Clerk SDK for those operations.
147
+
148
+ ## Guest And Authenticated Behavior
149
+
150
+ Guest buyer:
151
+
152
+ - can browse, use the cart, and submit checkout;
153
+ - does not receive an account silently;
154
+ - can use the public order lookup token returned by checkout.
155
+
156
+ Authenticated buyer:
157
+
158
+ - uses the same cart and checkout methods;
159
+ - is associated by Bacano from verified Hasura session variables;
160
+ - can read and update their Bacano customer profile;
161
+ - can read only their own `WEB` orders for the exact website.
162
+
163
+ Checkout code is identical in both modes:
164
+
165
+ ```ts
166
+ const result = await client.checkout.submit({
167
+ contact: {
168
+ firstName: "Maria",
169
+ lastName: "Perez",
170
+ email: "maria@example.com",
171
+ cellphone: "+573001234567",
172
+ },
173
+ fulfillmentFamily: "SHIPPING",
174
+ address,
175
+ shippingQuoteId,
176
+ paymentMethodId,
177
+ });
178
+ ```
179
+
180
+ Do not add a `clientId` to the checkout payload. If Clerk has an active
181
+ session, the SDK sends its token automatically. Otherwise checkout remains a
182
+ guest checkout.
183
+
184
+ If checkout collects a mobile/WhatsApp number that is not yet in the Bacano
185
+ profile, synchronize it explicitly through the authenticated profile API:
186
+
187
+ ```ts
188
+ await client.profile.update({ cellphone: contact.phone });
189
+ await client.checkout.submit({
190
+ contact,
191
+ fulfillmentFamily: "SHIPPING",
192
+ address,
193
+ shippingQuoteId,
194
+ paymentMethodId,
195
+ });
196
+ ```
197
+
198
+ This is an explicit commercial-profile update, not identity matching. Checkout
199
+ never accepts `clientId`; Bacano associates the order from the verified Clerk
200
+ token. Guest checkout continues without this call or an access token.
201
+
202
+ ## Deferred Account Activation From Checkout
203
+
204
+ A guest may explicitly request an account without interrupting payment or
205
+ email verification during checkout:
206
+
207
+ ```ts
208
+ const result = await client.checkout.submit({
209
+ contact,
210
+ fulfillmentFamily: "SHIPPING",
211
+ address,
212
+ shippingQuoteId,
213
+ paymentMethodId,
214
+ buyerAccountOptIn: true,
215
+ });
216
+
217
+ console.log(result.buyerAccountInvitationRequested);
218
+ ```
219
+
220
+ `buyerAccountOptIn` is only a consent signal. After Bacano validates and
221
+ persists a real guest order, it queues the Clerk application invitation and
222
+ returns the checkout response without waiting for Clerk. The invitation does
223
+ not depend on payment approval: pending bank transfers, cash on delivery and
224
+ payment-session orders are eligible as soon as the order exists. Invalid
225
+ checkout attempts that never create an order do not send invitations. Failed
226
+ invitation delivery never rolls back or changes the order.
227
+
228
+ After Clerk verifies the buyer and accepts the activation reference, Bacano
229
+ atomically assigns the order to that website-scoped buyer and fills only
230
+ missing commercial profile fields from the persisted checkout snapshot. This
231
+ includes name, mobile and primary shipping address. Existing profile data is
232
+ preserved, and the authenticated email always comes from Clerk rather than the
233
+ checkout payload.
234
+
235
+ The storefront must show this option only after the buyer explicitly checks a
236
+ label such as "Crear mi cuenta para consultar este pedido después". Do not
237
+ preselect it. If the buyer is already authenticated, Bacano ignores the option
238
+ and associates the order from the verified Clerk token instead.
239
+
240
+ The invitation redirects to the website's configured activation path (default
241
+ `/crear-cuenta`) with Clerk's `__clerk_ticket` query parameter. That page must
242
+ complete Clerk's official invitation flow. After Clerk establishes a session,
243
+ make an authenticated SDK request such as `client.profile.get()`. Bacano then
244
+ links the order using the backend-issued activation reference, the exact
245
+ website and company, and Clerk's verified primary email. The storefront never
246
+ sends an order claim, email match, invitation id, password, code, or secret.
247
+
248
+ `buyerAccountInvitationRequested` confirms that Bacano durably accepted the
249
+ request; it does not mean the email has already been delivered, that the buyer
250
+ has activated the account, or that payment was approved.
251
+
252
+ ## Profile And Orders
253
+
254
+ These methods require an authenticated Clerk session:
255
+
256
+ ```ts
257
+ const profile = await client.profile.get();
258
+ const addresses = await client.profile.getAddresses();
259
+
260
+ await client.profile.update({
261
+ name: "Ada",
262
+ lastname: "Lovelace",
263
+ cellphone: "+573001234567",
264
+ address: "Carrera 10 # 20-30",
265
+ addressComplement: "Apto 401",
266
+ countryCode: "CO",
267
+ departmentId: "68",
268
+ municipalityId: "68001",
269
+ });
270
+
271
+ const orders = await client.orders.list({ limit: 20, offset: 0 });
272
+ const order = await client.orders.get(orders[0].id);
273
+ ```
274
+
275
+ Account pages must handle an expired or missing Clerk session by asking the
276
+ buyer to sign in again. They must not fall back to a public GraphQL query.
277
+
278
+ `profile.email` is the verified primary email synchronized by Bacano directly
279
+ from Clerk. It is read-only: storefronts must not send email, passwords,
280
+ verification codes, tokens, or any other identity secret to `profile.update()`.
281
+
282
+ Use `cellphone` as the canonical mobile/WhatsApp number in E.164 format
283
+ (`+573001234567`). `phone` is reserved for an optional secondary or
284
+ national-format telephone number. A WhatsApp opt-in, if added later, must be a
285
+ separate consent field rather than another alias for the same number.
286
+
287
+ Calling `profile.update()` repeatedly is safe: it updates the authenticated
288
+ buyer's existing party and never creates or matches customers by email.
289
+
290
+ `profile.getAddresses()` resolves the customer exclusively from the verified
291
+ Clerk token and current website. It returns only checkout-ready commercial
292
+ (`PRIMARY`) and saved delivery (`SHIPPING`) addresses whose department and
293
+ municipality are canonical. Incomplete legacy addresses are omitted so the
294
+ storefront requests location again instead of matching it by text. Use
295
+ `departmentId`/`municipalityId` directly when pre-filling checkout and quoting
296
+ shipping; do not send a `clientId`.
297
+
298
+ ## Server Rendering And Static Sites
299
+
300
+ Public catalog and content requests may run without an access token.
301
+
302
+ For an authenticated server-side request, use Clerk's server auth API to
303
+ provide a token for that request and forward the storefront's exact `Origin`.
304
+ Never reuse one buyer token across requests or cache authenticated responses
305
+ between users.
306
+
307
+ Static storefronts should initialize Clerk in the browser from the public
308
+ website bootstrap. Do not bake Clerk secret keys or buyer tokens into static
309
+ output.
310
+
311
+ ## Required Configuration
312
+
313
+ Before testing a website:
314
+
315
+ 1. Bacano website auth provisioning must be `READY`.
316
+ 2. The website must contain its Clerk publishable key and JWKS URL.
317
+ 3. The storefront origin must be configured in Bacano.
318
+ 4. The same origin must be allowed by Clerk.
319
+ 5. Redirect URLs for sign-in, sign-up, verification, and recovery must match
320
+ the storefront environment.
321
+ 6. Local origins must include the exact port.
322
+
323
+ ## Acceptance Checklist
324
+
325
+ Guest mode:
326
+
327
+ - catalog and content load;
328
+ - cart persists;
329
+ - checkout succeeds without creating an account.
330
+
331
+ Account mode:
332
+
333
+ - sign-up and email verification succeed through Clerk;
334
+ - sign-in, refresh, sign-out, and password recovery work;
335
+ - authenticated checkout appears in `orders.list()`;
336
+ - profile reads and updates work.
337
+
338
+ Isolation:
339
+
340
+ - a token issued for another Clerk application is rejected;
341
+ - a token with a different `azp` origin is rejected;
342
+ - buyers cannot read another website's orders;
343
+ - POS and physical-store orders never appear in the storefront account.
344
+
345
+ Regression:
346
+
347
+ - guest checkout still works after Clerk is enabled;
348
+ - deferred account opt-in does not delay or fail checkout;
349
+ - an accepted invitation exposes its order only after an authenticated SDK
350
+ request completes the secure claim;
351
+ - payment-session flows still work;
352
+ - catalog, content, media, and public order lookup remain public where
353
+ intended.
354
+
355
+ ## Deliberately Unsupported
356
+
357
+ - Silent account creation from checkout contact fields.
358
+ - Claiming old guest orders by email match alone.
359
+ - Sharing one Clerk account or password across independent websites.
360
+ - Storefront-managed JWT persistence.
361
+ - Direct Hasura access from the storefront.
362
+
363
+ Only orders carrying an explicit deferred-account opt-in can be claimed later,
364
+ and Bacano performs that claim from backend-issued invitation metadata plus a
365
+ verified Clerk identity.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pulpo Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.