@asteby/storefront 0.10.0 → 0.11.1
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 +151 -4
- package/dist/cli.js +700 -0
- package/dist/fiscal-document.cjs +659 -0
- package/dist/fiscal-document.cjs.map +1 -0
- package/dist/fiscal-document.d.cts +215 -0
- package/dist/fiscal-document.d.ts +215 -0
- package/dist/fiscal-document.js +641 -0
- package/dist/fiscal-document.js.map +1 -0
- package/dist/index.cjs +97 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -5
- package/dist/index.d.ts +61 -5
- package/dist/index.js +95 -6
- package/dist/index.js.map +1 -1
- package/dist/mock.cjs +1072 -11
- package/dist/mock.cjs.map +1 -1
- package/dist/mock.d.cts +33 -2
- package/dist/mock.d.ts +33 -2
- package/dist/mock.js +1069 -12
- package/dist/mock.js.map +1 -1
- package/dist/mockup.d.cts +1 -1
- package/dist/mockup.d.ts +1 -1
- package/dist/node.cjs +297 -0
- package/dist/node.cjs.map +1 -0
- package/dist/node.d.cts +134 -0
- package/dist/node.d.ts +134 -0
- package/dist/node.js +260 -0
- package/dist/node.js.map +1 -0
- package/dist/{types-COSwo4ND.d.ts → types-B1E3GySa.d.ts} +293 -3
- package/dist/{types-DdLzlxw4.d.cts → types-bsdlAumU.d.cts} +293 -3
- package/package.json +41 -3
package/README.md
CHANGED
|
@@ -8,7 +8,9 @@ and CommonJS builds with types.
|
|
|
8
8
|
|
|
9
9
|
Includes `@asteby/storefront/mock`: an in-memory implementation of the whole
|
|
10
10
|
contract (catalog with facets, carts, discounts, delivery, checkout, OTP
|
|
11
|
-
login, content) to build a store without OPS
|
|
11
|
+
login, content, region and invoices) to build a store without OPS, and
|
|
12
|
+
`@asteby/storefront/fiscal-document`: reads a buyer's fiscal document (PDF)
|
|
13
|
+
in the browser or Node with rules published by the region.
|
|
12
14
|
|
|
13
15
|
React bindings (cart, prices, images, SEO, sections, theme) live in
|
|
14
16
|
[`@asteby/storefront-react`](../storefront-react).
|
|
@@ -301,6 +303,151 @@ Pure helpers, no DOM: `visitorCountry(headers)` (CDN country headers),
|
|
|
301
303
|
(suggests, never redirects; respects crawlers and a saved choice; country wins
|
|
302
304
|
over language) and `hreflangLinks` for per-market SEO.
|
|
303
305
|
|
|
306
|
+
## Fiscal identity and invoices (Storefront API 1.6)
|
|
307
|
+
|
|
308
|
+
The buyer's tax data travels in four pieces, all country-agnostic. What changes per country is DATA the region
|
|
309
|
+
publishes; the SDK knows no tax authority.
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
const region = await sf.region() // GET /storefront/region (null on 404)
|
|
313
|
+
const fid = region?.fiscal_id // { kind, label, pattern, generic_ids, regimes[], invoice_uses[], documents[] }
|
|
314
|
+
|
|
315
|
+
// Ask for an invoice in the checkout. 422 names the exact field: `invoice.tax_id`, `invoice.tax_regime`, `invoice.use`...
|
|
316
|
+
await sf.carts.setInvoice(cart.id, { tax_id, legal_name, tax_regime, use, postal_code, document })
|
|
317
|
+
await sf.carts.setInvoice(cart.id, null) // withdraw (sends { requested: false })
|
|
318
|
+
// checkout uses `carts.invoice` when `CheckoutInput.invoice` is omitted; both are validated BEFORE any charge.
|
|
319
|
+
|
|
320
|
+
await customer.customer.update({ fiscal: { ...data } }) // save to the account (PATCH /customer)
|
|
321
|
+
await customer.customer.update({ fiscal: null }) // delete it (ARCO)
|
|
322
|
+
await customer.customer.requestInvoice(order.id, data) // later; order.invoice.stage / can_request / action drive the UI
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
`customer.capabilities().invoicing` says whether the store invoices (`available`, `countries`, `checkout`,
|
|
326
|
+
`post_purchase`, `identity_documents`). Types: `Region`, `RegionFiscalId`, `IdentityDocumentSpec`, `FiscalIdentity`,
|
|
327
|
+
`FiscalDocumentEvidence`, `InvoiceSummary` (`stage`, `reason_code`, `action`, `can_request`, `blocked_reason`, `url`,
|
|
328
|
+
`xml_url`), `CartInvoice`, `InvoicingCapabilities`.
|
|
329
|
+
|
|
330
|
+
### Reading the buyer's document (`@asteby/storefront/fiscal-document`)
|
|
331
|
+
|
|
332
|
+
The buyer can upload the authority's PDF (in Mexico, the constancia de situación fiscal) to prefill the form. It is
|
|
333
|
+
read **on the buyer's device** (or in memory on your server for the no-JS path); the PDF is never uploaded, stored or
|
|
334
|
+
logged. Only the fields the buyer reviews, plus minimal evidence (document id, SHA-256, extractor, QR match), travel.
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
import { readPdfText, extractIdentityDocument, decodePdfQr, buildDocumentEvidence, catalogsFromRegion, isIdentityDocumentError } from '@asteby/storefront/fiscal-document'
|
|
338
|
+
|
|
339
|
+
const spec = region.fiscal_id.documents.find((d) => d.kind === 'MX:csf')!
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
const { text, sha256 } = await readPdfText(file, { workerSrc: '/assets/pdf.worker.mjs', maxBytes: spec.max_bytes, maxPages: spec.max_pages }) // browser: YOUR worker URL
|
|
343
|
+
const qr = await decodePdfQr(file, { workerSrc: '/assets/pdf.worker.mjs' }) // optional; null if it cannot (the URL the QR encodes)
|
|
344
|
+
const x = extractIdentityDocument(text, spec, { catalogs: catalogsFromRegion(region.fiscal_id), qr })
|
|
345
|
+
x.fields // { tax_id, legal_name, postal_code, tax_regime: ['612', '605'], document_id, issued_on } (what was read)
|
|
346
|
+
x.missing // fields to leave empty for the buyer to type
|
|
347
|
+
x.qr.match // true | false | null (no QR / nothing to compare)
|
|
348
|
+
x.stale // older than spec.freshness_days
|
|
349
|
+
const document = buildDocumentEvidence(x, { sha256, source: 'pdf_client' }) // goes in InvoiceRequest.document
|
|
350
|
+
} catch (err) {
|
|
351
|
+
if (isIdentityDocumentError(err)) err.code // 'too_large' | 'not_pdf' | 'unreadable' | 'no_text' | 'password'
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
- **Sensitive data never comes out.** The spec's `discard_fields` (a national person id, street, activities...) are
|
|
356
|
+
removed from the output whatever produced them (rules may not even declare them). The extraction carries no
|
|
357
|
+
text and no error message contains document text.
|
|
358
|
+
- **pdf.js is loaded on demand** (`import('pdfjs-dist/legacy/build/pdf.mjs')`) with `isEvalSupported: false`; the
|
|
359
|
+
main entry point does not include it. Install `pdfjs-dist@>=4.2.67` (older versions are affected by CVE-2024-4367).
|
|
360
|
+
In browsers there is deliberately **no default worker and no CDN**: copy `pdf.worker.mjs` to your own assets and pass
|
|
361
|
+
`workerSrc` (or `setPdfjsLoader` to configure it once). Allow it in your CSP with `worker-src 'self'`. Node needs no worker file.
|
|
362
|
+
- **Limits** (`readPdfText` options): `maxBytes` 5 MiB, `maxPages` 6, `timeoutMs` 3000, `%PDF-` signature checked first.
|
|
363
|
+
Scans have no text layer: `no_text` means "ask for manual entry" (no OCR).
|
|
364
|
+
- **`decodePdfQr`** is best effort: `BarcodeDetector` or the optional `jsqr` package (both on demand) on a rendered page.
|
|
365
|
+
Without a canvas (Node) it returns `null` unless you pass your own `decode(imageData)`. It never blocks the flow.
|
|
366
|
+
- **Countries with code**: `registerIdentityExtractor('<id or kind>', (text, spec, options) => ({ fields, qr }))`
|
|
367
|
+
for a spec with `engine: 'plugin:<id>'`. `discard_fields` still applies to it. The server-side validator is the region's.
|
|
368
|
+
|
|
369
|
+
#### `rules@1`
|
|
370
|
+
|
|
371
|
+
The generic engine runs the rules of the spec (`spec.rules`; the API publishes an object, JSON text is accepted).
|
|
372
|
+
It is the TypeScript twin of the reference interpreter of the region packs (`regionspec.ExtractIdentity`): same
|
|
373
|
+
semantics, held to the same synthetic documents. The text is normalized (`nbsp`, `collapse_ws`), then each field
|
|
374
|
+
tries its strategies in order and the first non-empty value wins.
|
|
375
|
+
|
|
376
|
+
```jsonc
|
|
377
|
+
{
|
|
378
|
+
"version": 1,
|
|
379
|
+
"normalize": ["nbsp", "collapse_ws"],
|
|
380
|
+
"labels": ["RFC", "CURP", "C[óo]digo Postal", "Reg[íi]menes", "Obligaciones"], // a labelled value ends at the next caption
|
|
381
|
+
"fields": [
|
|
382
|
+
{ "key": "tax_id", "required": true, "strategies": [
|
|
383
|
+
{ "strategy": "labeled_lines", "label": "RFC", "upper": true, "capture": { "pattern": "\\b([A-Z]{4}[0-9]{6}[A-Z0-9]{3})\\b", "group": 1 } },
|
|
384
|
+
{ "strategy": "qr_param", "param": "D3", "split": "_", "parts": 2, "match": "^[A-Z]{4}[0-9]{6}[A-Z0-9]{3}$" } ] },
|
|
385
|
+
{ "key": "tax_regime", "multiple": true, "strategies": [
|
|
386
|
+
{ "strategy": "catalog_phrases", "catalog": "fiscal_regime", "start": "Reg[íi]menes\\s*:?", "end": "Obligaciones\\s*:" } ] }
|
|
387
|
+
]
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
| Strategy | Reads |
|
|
392
|
+
|---|---|
|
|
393
|
+
| `regex` | `group` (0 = whole match) of the first match of `pattern` (`flags`: `i`, `s`, `m`) |
|
|
394
|
+
| `labeled_lines` | the value after the caption `label` up to the next caption of `labels`, line breaks or not |
|
|
395
|
+
| `concat_labeled` | the values of several captions joined with `separator` (first names + surnames) |
|
|
396
|
+
| `catalog_phrases` | the codes of the catalog entries whose phrases appear between `start` and `end`, in order of first mention. Phrases come from `match_phrases`, or the label when absent. No `start` found: no section |
|
|
397
|
+
| `date` | `pattern` with three groups in the order of `order` (`dmy`, `ymd`...); `months` (12 names) reads a month in text; calendar-invalid dates are dropped. Result `YYYY-MM-DD` |
|
|
398
|
+
| `qr_param` | from the QR URL: parameter `param`, split on `split` into exactly `parts` parts; the part matching `match` (uppercased). `scan` (+ `flags`, `group`) is the fallback for QRs read by a keyboard-wedge scanner that garbled the punctuation: a pattern over the raw text |
|
|
399
|
+
|
|
400
|
+
Any strategy takes `upper` and `capture` (`{pattern, flags, group}`: the value must match it and the group replaces
|
|
401
|
+
the value). **The text wins over the QR**; a `qr_param` strategy only fills what the text lacks, and `extraction.qr.match`
|
|
402
|
+
is `false` when both carry a field and differ (a warning, never a block). Patterns are limited to the subset common to Go and
|
|
403
|
+
JavaScript (no lookaround, backreferences, named groups, inline flags or `\p`); `validateRules` rejects the rest, and
|
|
404
|
+
a rule that produces a `discard_fields` field. `qr_canonical` (top level) is a template, e.g. `https://host/path?D3={document_id}_{tax_id}`, filled with what the QR carried; `extraction.qr.canonical` returns it rebuilt (always the canonical form, however the scanner garbled it). Catalogs for `catalog_phrases` come from `catalogsFromRegion(region.fiscal_id)`
|
|
405
|
+
(`fiscal_regime`, `invoice_use`). A text that does not match yields `missing` (required fields); only a spec that cannot
|
|
406
|
+
run throws `IdentityRulesError`.
|
|
407
|
+
|
|
408
|
+
## Connecting the store to OPS (`connect`, `diagnose`, token files)
|
|
409
|
+
|
|
410
|
+
A server-side store (`sk_`) is connected to OPS with a one-time code, never by copying a secret around.
|
|
411
|
+
|
|
412
|
+
```sh
|
|
413
|
+
# on the store's server; the code (K7QM-2XPA, 10 min, single use) is typed at the prompt, never an argument
|
|
414
|
+
npx @asteby/storefront connect --ops https://ops.example.com --env-file .env --token-file /etc/mystore/storefront_token
|
|
415
|
+
# Conectada ✓ · 7 alcances · sk_…a91f
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
`connect` redeems the code (`POST {ops}/api/connect/v1/redeem` with `{code, client:{name,version,runtime,hostname}}`; OPS
|
|
419
|
+
answers `{success:true,data:{token, connection, scopes, rotation_days, api_base}}`, an invalid, expired or used code is one
|
|
420
|
+
answer: `400 invalid_code`), writes the key ATOMICALLY (temp file + rename, mode `0600`; default `./storefront_token`)
|
|
421
|
+
with its non-secret state beside it (`<file>.json`: connection, `api_base`, `rotation_days`, `rotate_after`), leaves ONLY `OPS_URL` and
|
|
422
|
+
`STOREFRONT_TOKEN_FILE` in the env file (never the key), prints just the prefix and last four characters, and checks
|
|
423
|
+
the key with `token.introspect()`. Exit code is `1` with an es-MX message for an expired or used code, an unreachable
|
|
424
|
+
OPS or a non-TTY stdin.
|
|
425
|
+
|
|
426
|
+
Read the key without restarting when it rotates (`@asteby/storefront/node`, Node only):
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
import { createStorefrontClient, diagnose } from '@asteby/storefront'
|
|
430
|
+
import { connectStore, storefrontTokenFromEnv } from '@asteby/storefront/node'
|
|
431
|
+
|
|
432
|
+
const storefront = createStorefrontClient({
|
|
433
|
+
baseUrl: `${process.env.OPS_URL}/api/gw`,
|
|
434
|
+
// STOREFRONT_TOKEN_FILE, then $CREDENTIALS_DIRECTORY/storefront_token (systemd LoadCredential=), then STOREFRONT_TOKEN.
|
|
435
|
+
// Files are re-read every 30 s (only when the mtime changed); a failed re-read keeps the last good key.
|
|
436
|
+
token: storefrontTokenFromEnv()!,
|
|
437
|
+
})
|
|
438
|
+
void diagnose(storefront) // one warning if scopes are missing; never throws, never delays boot
|
|
439
|
+
connectStore().keepFresh() // heartbeat; rotates (POST /api/connect/v1/rotate) on rotate_now / next_rotation_at; honors Retry-After on 429
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
`token` also accepts any `() => string`, called on every request. `diagnose` logs, at most once, which scopes are
|
|
443
|
+
missing and where to repair them (`OPS → Conexiones y API → <conexión> → Reparar permisos`), showing `sk_…a91f` and never
|
|
444
|
+
the key. Repairing permissions does not change the key.
|
|
445
|
+
|
|
446
|
+
Errors: a `403 insufficient_scope` is a `StorefrontError` with `code: 'insufficient_scope'`, `status: 403` and
|
|
447
|
+
`requiredScopes: ['customer:write']`. `customer.capabilities()` reports `password: false`, `unavailable_reason:
|
|
448
|
+
'missing_scope'` and `missing_scopes` when the key cannot create accounts. The mock rehearses all of it with
|
|
449
|
+
`createMockStorefront({ missingScopes: ['customer:write'] })`.
|
|
450
|
+
|
|
304
451
|
## API
|
|
305
452
|
|
|
306
453
|
`createStorefrontClient(options)`:
|
|
@@ -320,10 +467,10 @@ over language) and `hreflangLinks` for per-market SEO.
|
|
|
320
467
|
| `onRequest?(ctx)` | Runs before each request; return a `Response` to short-circuit (SSR cache hit) |
|
|
321
468
|
| `onResponse?(ctx)` | Runs after each response (cache store, logging, metrics) |
|
|
322
469
|
|
|
323
|
-
Resources: `shop.get`, `products.list/get/recommendations`,
|
|
470
|
+
Resources: `shop.get`, `region`, `products.list/get/recommendations`,
|
|
324
471
|
`collections.list/get`, `stock.get`,
|
|
325
|
-
`carts.create/get/addLines/updateLines/removeLines/setDiscountCodes/setBuyer/setShippingAddress/deliveryOptions/setDelivery/checkout`,
|
|
326
|
-
`customer.requestOtp/createSession/capabilities/register/verifyRegistration/loginWithPassword/setPassword/resetPassword/get/orders/order/requestReturn/requestInvoice/addresses.{list,create,update,delete}`,
|
|
472
|
+
`carts.create/get/addLines/updateLines/removeLines/setDiscountCodes/setBuyer/setShippingAddress/deliveryOptions/setDelivery/setInvoice/checkout`,
|
|
473
|
+
`customer.requestOtp/createSession/capabilities/register/verifyRegistration/loginWithPassword/setPassword/resetPassword/get/update/orders/order/requestReturn/requestInvoice/addresses.{list,create,update,delete}`,
|
|
327
474
|
`content.theme/page/menu/articles/article`. Every method takes a last
|
|
328
475
|
`options` argument: `{ cache, signal, next, headers, timeoutMs, customerToken, clientIp }`
|
|
329
476
|
(plus `idempotencyKey` on writes and `preview` on pages/articles); unknown keys
|