@blamejs/blamejs-shop 0.1.12 → 0.1.14
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/CHANGELOG.md +4 -0
- package/lib/address-validation.js +8 -12
- package/lib/addresses.js +4 -8
- package/lib/admin.js +22 -26
- package/lib/affiliates.js +18 -24
- package/lib/analytics.js +4 -9
- package/lib/announcement-bar.js +7 -11
- package/lib/api-keys.js +15 -21
- package/lib/assembly-instructions.js +9 -13
- package/lib/auto-discount.js +3 -7
- package/lib/auto-replenish.js +5 -9
- package/lib/backorder.js +5 -9
- package/lib/banner-ab-tests.js +11 -15
- package/lib/barcodes.js +5 -9
- package/lib/bin-locations.js +4 -8
- package/lib/blog-articles.js +13 -17
- package/lib/bundles.js +6 -10
- package/lib/business-hours.js +3 -7
- package/lib/captcha-gate.js +5 -11
- package/lib/carrier-accounts.js +16 -22
- package/lib/carrier-rates.js +7 -13
- package/lib/cart-abandonment.js +9 -13
- package/lib/cart-bulk-ops.js +9 -13
- package/lib/cart-recovery.js +9 -13
- package/lib/cart.js +7 -11
- package/lib/catalog-drafts.js +8 -10
- package/lib/catalog-import.js +3 -7
- package/lib/catalog.js +13 -20
- package/lib/category-navigation.js +3 -7
- package/lib/checkout.js +3 -7
- package/lib/click-and-collect.js +7 -11
- package/lib/clickstream.js +9 -13
- package/lib/cms-blocks.js +5 -9
- package/lib/code-minter.js +5 -9
- package/lib/collections.js +10 -18
- package/lib/compliance-export.js +5 -9
- package/lib/config.js +3 -7
- package/lib/consent-ledger.js +5 -12
- package/lib/cookie-consent.js +6 -13
- package/lib/cost-layers.js +6 -10
- package/lib/coupon-stacking.js +2 -6
- package/lib/credit-limits.js +5 -9
- package/lib/currency-display.js +6 -10
- package/lib/currency-rounding.js +4 -8
- package/lib/customer-activity.js +6 -10
- package/lib/customer-impersonation.js +11 -18
- package/lib/customer-import.js +9 -13
- package/lib/customer-merge.js +9 -12
- package/lib/customer-notes.js +18 -22
- package/lib/customer-portal.js +10 -17
- package/lib/customer-risk-profile.js +5 -20
- package/lib/customer-roles.js +5 -9
- package/lib/customer-segments.js +7 -11
- package/lib/customer-surveys.js +10 -14
- package/lib/customers.js +10 -14
- package/lib/cycle-counting.js +5 -9
- package/lib/damage-photos.js +6 -10
- package/lib/delivery-estimate.js +8 -15
- package/lib/demand-forecast.js +7 -11
- package/lib/discount-allocation.js +6 -10
- package/lib/discount-analytics.js +8 -12
- package/lib/dispute-resolution.js +4 -8
- package/lib/dropship-forwarding.js +7 -11
- package/lib/dunning.js +6 -10
- package/lib/email-ab-tests.js +5 -9
- package/lib/email-campaigns.js +8 -12
- package/lib/email-engagement-score.js +4 -19
- package/lib/email-suppressions.js +7 -14
- package/lib/email-templates.js +4 -8
- package/lib/email-warmup.js +6 -13
- package/lib/email.js +5 -9
- package/lib/error-log.js +3 -8
- package/lib/event-log.js +7 -11
- package/lib/experiments.js +5 -9
- package/lib/externaldb-d1.js +2 -6
- package/lib/fraud-screen.js +10 -29
- package/lib/fulfillment-sla.js +5 -9
- package/lib/geolocation.js +2 -6
- package/lib/gift-card-ledger.js +6 -10
- package/lib/gift-options.js +4 -8
- package/lib/gift-registry.js +7 -11
- package/lib/giftcards.js +9 -13
- package/lib/inventory-alerts.js +4 -8
- package/lib/inventory-allocations.js +3 -7
- package/lib/inventory-audits.js +5 -9
- package/lib/inventory-locations.js +3 -7
- package/lib/inventory-receive.js +7 -11
- package/lib/inventory-snapshots.js +11 -15
- package/lib/inventory-writeoffs.js +6 -10
- package/lib/invoice-renderer.js +6 -10
- package/lib/knowledge-base.js +14 -18
- package/lib/line-gift-wrap.js +8 -15
- package/lib/live-chat.js +11 -18
- package/lib/locale-router.js +3 -7
- package/lib/loyalty-earn-rules.js +4 -8
- package/lib/loyalty-redemption.js +5 -9
- package/lib/loyalty.js +4 -8
- package/lib/mailing-audiences.js +5 -9
- package/lib/marketing-budget.js +6 -10
- package/lib/metered-usage.js +4 -8
- package/lib/newsletter.js +14 -24
- package/lib/notifications.js +5 -9
- package/lib/operator-activity-feed.js +6 -10
- package/lib/operator-approvals.js +4 -8
- package/lib/operator-audit-log.js +5 -9
- package/lib/operator-help-center.js +13 -17
- package/lib/operator-inbox.js +8 -12
- package/lib/operator-roles.js +4 -8
- package/lib/operator-sessions.js +12 -18
- package/lib/order-escalation.js +6 -10
- package/lib/order-exchanges.js +4 -8
- package/lib/order-export.js +10 -14
- package/lib/order-notes.js +14 -20
- package/lib/order-ratings.js +5 -9
- package/lib/order-tags.js +7 -11
- package/lib/order-timeline.js +10 -14
- package/lib/order-tracking.js +5 -9
- package/lib/order.js +11 -15
- package/lib/packing-slips.js +7 -11
- package/lib/payment-methods.js +5 -9
- package/lib/payment-retries.js +6 -10
- package/lib/payment.js +257 -13
- package/lib/pick-lists.js +5 -9
- package/lib/pixel-events.js +6 -10
- package/lib/plan-changes.js +4 -8
- package/lib/preorder.js +5 -9
- package/lib/price-display.js +7 -11
- package/lib/pricing.js +2 -6
- package/lib/print-on-demand.js +27 -31
- package/lib/print-queue.js +6 -10
- package/lib/print-receipts.js +6 -10
- package/lib/product-bulk-ops.js +4 -8
- package/lib/product-compare.js +7 -11
- package/lib/product-import.js +11 -15
- package/lib/product-qa.js +10 -17
- package/lib/promo-banners.js +13 -17
- package/lib/promo-bundles.js +7 -11
- package/lib/purchase-orders.js +7 -13
- package/lib/push-notifications.js +12 -16
- package/lib/pwa-manifest.js +7 -11
- package/lib/quantity-discounts.js +8 -12
- package/lib/quotes.js +5 -12
- package/lib/r2-bridge.js +2 -6
- package/lib/recently-viewed.js +6 -14
- package/lib/recommendations.js +7 -15
- package/lib/referral-leaderboard.js +5 -9
- package/lib/referrals.js +8 -12
- package/lib/refund-automation.js +5 -10
- package/lib/refund-policy.js +4 -9
- package/lib/reorder-reminders.js +8 -12
- package/lib/reorder-thresholds.js +5 -9
- package/lib/return-labels.js +6 -10
- package/lib/returns.js +10 -14
- package/lib/reviews.js +8 -15
- package/lib/robots-config.js +5 -10
- package/lib/sales-reports.js +7 -11
- package/lib/sales-tax-filings.js +10 -14
- package/lib/save-for-later.js +8 -12
- package/lib/search-facets.js +15 -22
- package/lib/search-ranking.js +4 -8
- package/lib/search-suggestions.js +8 -16
- package/lib/search-synonyms.js +9 -15
- package/lib/seller-signup.js +9 -16
- package/lib/shipping-insurance.js +6 -10
- package/lib/shipping-labels.js +6 -10
- package/lib/shipping-zones.js +2 -9
- package/lib/shrinkage-report.js +9 -13
- package/lib/sidebar-widgets.js +8 -12
- package/lib/site-redirects.js +4 -11
- package/lib/sitemap-generator.js +5 -9
- package/lib/smart-restocking.js +3 -7
- package/lib/sms-dispatcher.js +13 -17
- package/lib/split-shipments.js +6 -10
- package/lib/stock-alerts.js +10 -19
- package/lib/stock-receipts.js +14 -18
- package/lib/stock-transfers.js +8 -12
- package/lib/store-credit.js +6 -9
- package/lib/storefront-dashboards.js +4 -8
- package/lib/storefront-forms.js +10 -14
- package/lib/storefront-pages.js +5 -9
- package/lib/storefront.js +46 -50
- package/lib/subscription-analytics.js +14 -22
- package/lib/subscription-billing.js +9 -13
- package/lib/subscription-controls.js +6 -9
- package/lib/subscription-gifts.js +12 -15
- package/lib/subscriptions.js +5 -9
- package/lib/suggestion-box.js +10 -14
- package/lib/support-tickets.js +18 -25
- package/lib/tax-cert-renewals.js +7 -10
- package/lib/tax-exempt.js +5 -9
- package/lib/tax-rates.js +6 -13
- package/lib/tax-remittance.js +6 -9
- package/lib/tenants.js +6 -13
- package/lib/theme-assets.js +5 -10
- package/lib/theme.js +3 -7
- package/lib/tier-benefits.js +4 -8
- package/lib/translations.js +7 -14
- package/lib/trust-badges.js +9 -14
- package/lib/variants.js +9 -16
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +4 -0
- package/lib/vendor/blamejs/README.md +2 -1
- package/lib/vendor/blamejs/api-snapshot.json +39 -2
- package/lib/vendor/blamejs/index.js +2 -0
- package/lib/vendor/blamejs/lib/content-digest.js +189 -0
- package/lib/vendor/blamejs/lib/structured-fields.js +362 -0
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.12.53.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.54.json +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +11 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/content-digest.test.js +87 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/structured-fields-codec.test.js +171 -0
- package/lib/vendor-invoices.js +8 -15
- package/lib/vendors.js +6 -12
- package/lib/webhook-receiver.js +13 -24
- package/lib/webhook-subscriptions.js +9 -16
- package/lib/webhooks.js +17 -20
- package/lib/winback-campaigns.js +8 -11
- package/lib/wishlist-alerts.js +9 -12
- package/lib/wishlist-digest.js +9 -12
- package/lib/wishlist-sharing.js +13 -17
- package/lib/wishlist.js +7 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.1.x
|
|
10
10
|
|
|
11
|
+
- v0.1.14 (2026-05-25) — **PayPal payment adapter (Orders v2).** `payment.create({ adapter: "paypal", … })` is a new native PayPal adapter alongside the Stripe one — a from-scratch Orders-v2 client over the framework's SSRF-gated HTTP client, with no PayPal SDK dependency. It exchanges an OAuth2 client-credentials token (cached until it nears expiry), creates and captures orders, fetches and refunds them, and verifies inbound webhooks through PayPal's verify-webhook-signature API. This ships the adapter only; wiring it into the checkout flow and a storefront button comes next. Card / Stripe checkout is unchanged. **Added:** *PayPal Orders-v2 adapter* — `payment.create({ adapter: "paypal", clientId, secret, sandbox?, webhookId?, apiBase? })` returns `{ createOrder, captureOrder, getOrder, refund, verifyWebhook }`. `createOrder({ amount_minor, currency, order_id?, return_url?, cancel_url? })` opens a CAPTURE-intent order (amounts converted to PayPal's decimal-string major units, including 0-decimal currencies); `captureOrder(id)` finalizes it; `refund({ capture_id, amount_minor?, currency? })` refunds full or partial; `getOrder(id)` reads status. Every call carries an OAuth2 bearer token exchanged once and cached until ~2 minutes before expiry, and a `PayPal-Request-Id` for idempotency (plus the shared idempotency cache when a `query` handle is wired). `verifyWebhook(headers, rawBody, { webhookId })` confirms an inbound event through PayPal's verify-webhook-signature API and returns `{ ok, event }`. Outbound HTTP goes through `b.httpClient` — no `paypal` npm dependency. Off until the operator supplies credentials; the Stripe adapter and existing checkout are unchanged.
|
|
12
|
+
|
|
13
|
+
- v0.1.13 (2026-05-25) — **Internal: uniform framework access across the library.** An internal consistency refactor with no API or behavior change. Every library module now captures the framework once at the top — straight from the vendored tree (`var b = require("./vendor/blamejs");`) — and uses `b.*` uniformly, replacing a per-module lazy accessor and its scattered call sites. Capturing the framework directly (rather than via the composing entry point) also avoids a circular-load edge case on leaf-first imports. Two source files that embedded a raw NUL byte as a map-key separator now use the `\u0000` escape, so the whole library is plain text. New lint rules lock all of this in place. Public APIs, runtime behavior, and the published surface are unchanged. **Added:** *Lint detectors for accessor uniformity and source hygiene* — Three repository lint rules now prevent drift: one rejects the reintroduction of the per-module lazy framework accessor (capture the framework once at module top); one rejects requiring the composing entry point from a leaf module (require the vendored framework directly — entry-point requires from a leaf create a circular-load hazard); and one rejects raw C0 control bytes in source files (write `\u0000` and friends as escapes so files stay plain text — grep / diff / editors handle them correctly). **Changed:** *Framework handle captured once per module* — Library modules previously reached the vendored framework through a lazy `_b()` accessor invoked at every call site. They now capture it once at module top — `var b = require("./vendor/blamejs");`, the same object the entry point re-exports as `.framework` — and reference `b.*` directly, matching how the edge worker already accesses it. Capturing it directly from the vendored tree (instead of through the composing entry point) keeps leaf-first module imports working — requiring a single module no longer pulls the entry point's whole assembly mid-initialization. No public API or runtime behavior changes.
|
|
14
|
+
|
|
11
15
|
- v0.1.12 (2026-05-25) — **Card payments now finalize the order — the Stripe webhook is handled end to end.** A confirmed Stripe payment now advances the order from pending to paid. The container now serves the `POST /api/webhooks/stripe` route the edge worker forwards to: it re-verifies the event signature over the exact raw bytes, maps the event to the order's FSM transition, and is idempotent across Stripe's re-deliveries. Previously the edge verified the webhook but nothing consumed it on the container, so a paid PaymentIntent (card, Apple Pay, or Google Pay) left the order stuck in pending — no fulfillment, no paid status. Operators running checkout should upgrade and confirm their Stripe webhook points at `/api/webhooks/stripe`. **Added:** *Raw-body capture for payment webhooks* — A small middleware preserves the exact request bytes for the webhook path before the JSON body-parser runs, so signature verification (which is computed over the raw body) is reliable. It is scoped to the webhook routes and leaves every other request untouched. **Fixed:** *Stripe webhook completes the order* — `POST /api/webhooks/stripe` is now handled on the container: the event signature is re-verified against `STRIPE_WEBHOOK_SECRET` over the raw request body (a tampered or unsigned event is rejected with 400), then `payment_intent.succeeded` / `.canceled` / `charge.refunded` drive the order FSM (`mark_paid` / `cancel` / `refund`). Re-deliveries are idempotent — an event for an order already in the target state is acknowledged with 200 and skipped. A delivery for an unknown PaymentIntent is acknowledged without effect. This closes the gap where a confirmed payment never moved the order out of `pending`.
|
|
12
16
|
|
|
13
17
|
- v0.1.11 (2026-05-25) — **Sign in with Apple.** Customers can now sign in with Apple, alongside passkeys and Google. A “Continue with Apple” button appears on the account login page once the operator wires the Apple credentials; the callback turns the verified Apple identity into a shop session, adopts the guest cart, and claims prior guest orders placed under the same verified email — the same way Google sign-in does. Apple's OAuth client secret is itself an ES256 JWT, which the shop mints from the team's Sign-in-with-Apple key. Sign in with Apple is off until the credentials are set, like every other integration. **Added:** *Sign in with Apple (OIDC)* — `GET /account/login/apple` starts the flow (sealed in-flight state cookie, PKCE, nonce); Apple posts the result back to `POST /account/auth/apple/callback` (response_mode=form_post). The callback verifies the state, exchanges the code, signs the customer in on `(provider=apple, subject)`, adopts the guest cart, and — when Apple reports the email as verified — claims prior guest orders under that email. The display name is captured from Apple's first-authorization `user` field (Apple sends it only once and never in the ID token). The button appears on `/account/login` only when the credentials are configured, and is listed on `/admin/integrations`. · *customers.mintAppleClientSecret* — Mints Apple's required ES256 client-secret JWT from a Services-ID `.p8` key (team id, key id, client id). This is the one signature the protocol forces to be classical ECDSA P-256 rather than the framework's post-quantum default — an external identity provider's wire format, not an application default. The secret is minted at boot with a 150-day life (inside Apple's six-month ceiling) and re-minted on each deploy. **Changed:** *Account login offers Apple when configured* — The login page shows Continue-with-Google and Continue-with-Apple buttons independently, each gated on its own credentials. Set `APPLE_TEAM_ID`, `APPLE_KEY_ID`, `APPLE_CLIENT_ID` (your Services ID), `APPLE_PRIVATE_KEY` (the `.p8` contents), and `SHOP_ORIGIN`, and add `<SHOP_ORIGIN>/account/auth/apple/callback` as a Return URL on the Services ID. Requires an Apple Developer Program membership. See the README “Optional integrations” section.
|
|
@@ -63,11 +63,7 @@
|
|
|
63
63
|
* @related b.crypto, b.safeJson, b.uuid, shop.addresses, shop.geolocation
|
|
64
64
|
*/
|
|
65
65
|
|
|
66
|
-
var
|
|
67
|
-
function _b() {
|
|
68
|
-
if (!bShop) bShop = require("./index");
|
|
69
|
-
return bShop.framework;
|
|
70
|
-
}
|
|
66
|
+
var b = require("./vendor/blamejs");
|
|
71
67
|
|
|
72
68
|
// ---- constants ----------------------------------------------------------
|
|
73
69
|
|
|
@@ -175,7 +171,7 @@ function _normalizedAddress(v) {
|
|
|
175
171
|
);
|
|
176
172
|
}
|
|
177
173
|
var json;
|
|
178
|
-
try { json =
|
|
174
|
+
try { json = b.safeJson.canonical(v); }
|
|
179
175
|
catch (e) {
|
|
180
176
|
throw new TypeError(
|
|
181
177
|
"addressValidation: normalized_address must be JSON-serializable — " + e.message
|
|
@@ -210,7 +206,7 @@ function _suggestions(v) {
|
|
|
210
206
|
throw new TypeError("addressValidation: suggestions must be an array");
|
|
211
207
|
}
|
|
212
208
|
var json;
|
|
213
|
-
try { json =
|
|
209
|
+
try { json = b.safeJson.canonical(v); }
|
|
214
210
|
catch (e) {
|
|
215
211
|
throw new TypeError(
|
|
216
212
|
"addressValidation: suggestions must be JSON-serializable — " + e.message
|
|
@@ -232,13 +228,13 @@ function _now() { return Date.now(); }
|
|
|
232
228
|
function _signatureForInput(input) {
|
|
233
229
|
_input(input, "input");
|
|
234
230
|
var canonical;
|
|
235
|
-
try { canonical =
|
|
231
|
+
try { canonical = b.safeJson.canonical(input); }
|
|
236
232
|
catch (e) {
|
|
237
233
|
throw new TypeError(
|
|
238
234
|
"addressValidation: input must be JSON-serializable — " + e.message
|
|
239
235
|
);
|
|
240
236
|
}
|
|
241
|
-
return
|
|
237
|
+
return b.crypto.namespaceHash(INPUT_NAMESPACE, canonical);
|
|
242
238
|
}
|
|
243
239
|
|
|
244
240
|
// ---- row -> wire conversions -------------------------------------------
|
|
@@ -292,7 +288,7 @@ function create(opts) {
|
|
|
292
288
|
opts = opts || {};
|
|
293
289
|
var query = opts.query;
|
|
294
290
|
if (!query) {
|
|
295
|
-
query = function (sql, params) { return
|
|
291
|
+
query = function (sql, params) { return b.externalDb.query(sql, params); };
|
|
296
292
|
}
|
|
297
293
|
|
|
298
294
|
async function _getValidationBySig(sig) {
|
|
@@ -354,7 +350,7 @@ function create(opts) {
|
|
|
354
350
|
return _rowToValidation(await _getValidationBySig(sig));
|
|
355
351
|
}
|
|
356
352
|
|
|
357
|
-
var id =
|
|
353
|
+
var id = b.uuid.v7();
|
|
358
354
|
await query(
|
|
359
355
|
"INSERT INTO address_validations " +
|
|
360
356
|
"(id, input_signature, normalized_address_json, deliverable, classification, " +
|
|
@@ -413,7 +409,7 @@ function create(opts) {
|
|
|
413
409
|
return _rowToSuggestion(await _getSuggestionByInput(partial));
|
|
414
410
|
}
|
|
415
411
|
|
|
416
|
-
var id =
|
|
412
|
+
var id = b.uuid.v7();
|
|
417
413
|
await query(
|
|
418
414
|
"INSERT INTO address_suggestions_cache " +
|
|
419
415
|
"(id, partial_input, suggestions_json, expires_at, occurred_at) " +
|
package/lib/addresses.js
CHANGED
|
@@ -50,11 +50,7 @@
|
|
|
50
50
|
* @related customers, checkout
|
|
51
51
|
*/
|
|
52
52
|
|
|
53
|
-
var
|
|
54
|
-
function _b() {
|
|
55
|
-
if (!bShop) bShop = require("./index");
|
|
56
|
-
return bShop.framework;
|
|
57
|
-
}
|
|
53
|
+
var b = require("./vendor/blamejs");
|
|
58
54
|
|
|
59
55
|
var MAX_LABEL_LEN = 64;
|
|
60
56
|
var MAX_RECIPIENT_NAME_LEN = 128;
|
|
@@ -72,7 +68,7 @@ var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
|
72
68
|
// ---- validators ---------------------------------------------------------
|
|
73
69
|
|
|
74
70
|
function _uuid(s, label) {
|
|
75
|
-
try { return
|
|
71
|
+
try { return b.guardUuid.sanitize(s, { profile: "strict" }); }
|
|
76
72
|
catch (e) { throw new TypeError("addresses: " + label + " — " + (e && e.message || "invalid UUID")); }
|
|
77
73
|
}
|
|
78
74
|
|
|
@@ -137,7 +133,7 @@ function create(opts) {
|
|
|
137
133
|
opts = opts || {};
|
|
138
134
|
var query = opts.query;
|
|
139
135
|
if (!query) {
|
|
140
|
-
query = function (sql, params) { return
|
|
136
|
+
query = function (sql, params) { return b.externalDb.query(sql, params); };
|
|
141
137
|
}
|
|
142
138
|
|
|
143
139
|
// Clear the named default flag on every non-archived row for this
|
|
@@ -180,7 +176,7 @@ function create(opts) {
|
|
|
180
176
|
var defShipping = _bool(input.is_default_shipping, "is_default_shipping");
|
|
181
177
|
var defBilling = _bool(input.is_default_billing, "is_default_billing");
|
|
182
178
|
|
|
183
|
-
var id =
|
|
179
|
+
var id = b.uuid.v7();
|
|
184
180
|
var ts = _now();
|
|
185
181
|
|
|
186
182
|
// Clear sibling defaults FIRST so the promotion lands cleanly.
|
package/lib/admin.js
CHANGED
|
@@ -32,11 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
var pricing = require("./pricing");
|
|
34
34
|
|
|
35
|
-
var
|
|
36
|
-
function _b() {
|
|
37
|
-
if (!bShop) bShop = require("./index");
|
|
38
|
-
return bShop.framework;
|
|
39
|
-
}
|
|
35
|
+
var b = require("./vendor/blamejs");
|
|
40
36
|
|
|
41
37
|
var AUDIT_NAMESPACE = "shop_admin";
|
|
42
38
|
|
|
@@ -86,7 +82,7 @@ function _parseLimit(str, label, max, fallback) {
|
|
|
86
82
|
|
|
87
83
|
function _htmlEscape(s) {
|
|
88
84
|
if (s == null) return "";
|
|
89
|
-
return
|
|
85
|
+
return b.template.escapeHtml(String(s));
|
|
90
86
|
}
|
|
91
87
|
|
|
92
88
|
// ---- bearer auth --------------------------------------------------------
|
|
@@ -102,7 +98,7 @@ function _readBearer(req) {
|
|
|
102
98
|
function _authOk(token, expected) {
|
|
103
99
|
if (typeof token !== "string" || typeof expected !== "string") return false;
|
|
104
100
|
if (token.length !== expected.length) return false;
|
|
105
|
-
return
|
|
101
|
+
return b.crypto.timingSafeEqual(token, expected);
|
|
106
102
|
}
|
|
107
103
|
|
|
108
104
|
// ---- admin browser session (sealed cookie) ------------------------------
|
|
@@ -118,8 +114,8 @@ var ADMIN_COOKIE_NAME = "shop_admin";
|
|
|
118
114
|
var _adminJarMemo = null;
|
|
119
115
|
function _adminJar() {
|
|
120
116
|
if (!_adminJarMemo) {
|
|
121
|
-
_adminJarMemo =
|
|
122
|
-
vault:
|
|
117
|
+
_adminJarMemo = b.cookies.create({
|
|
118
|
+
vault: b.vault,
|
|
123
119
|
defaults: { httpOnly: true, secure: true, sameSite: "Strict", path: "/admin" },
|
|
124
120
|
});
|
|
125
121
|
}
|
|
@@ -129,8 +125,8 @@ function _adminJar() {
|
|
|
129
125
|
function _setAdminCookie(res) {
|
|
130
126
|
_adminJar().writeSealed(res, ADMIN_COOKIE_NAME, JSON.stringify({
|
|
131
127
|
admin: true,
|
|
132
|
-
exp: Date.now() +
|
|
133
|
-
}), { expires: new Date(Date.now() +
|
|
128
|
+
exp: Date.now() + b.constants.TIME.hours(12),
|
|
129
|
+
}), { expires: new Date(Date.now() + b.constants.TIME.hours(12)) });
|
|
134
130
|
}
|
|
135
131
|
function _clearAdminCookie(res) {
|
|
136
132
|
_adminJar().clear(res, ADMIN_COOKIE_NAME);
|
|
@@ -153,7 +149,7 @@ function _htmlAuthed(req, expectedToken) {
|
|
|
153
149
|
}
|
|
154
150
|
|
|
155
151
|
function _problem(res, status, code, detail) {
|
|
156
|
-
return
|
|
152
|
+
return b.problemDetails.send(res, {
|
|
157
153
|
type: "/problems/" + code,
|
|
158
154
|
title: code.replace(/-/g, " "),
|
|
159
155
|
status: status,
|
|
@@ -193,7 +189,7 @@ function _wrap(handler, opts) {
|
|
|
193
189
|
// write path it observes. Equivalent to `try { audit.emit(...)
|
|
194
190
|
// } catch (_e) {}` but composed via the framework primitive
|
|
195
191
|
// instead of a local wrapper.
|
|
196
|
-
|
|
192
|
+
b.audit.safeEmit({
|
|
197
193
|
action: AUDIT_NAMESPACE + "." + opts.audit,
|
|
198
194
|
outcome: "success",
|
|
199
195
|
metadata: { id: result.id || null },
|
|
@@ -231,7 +227,7 @@ function mount(router, deps) {
|
|
|
231
227
|
// into every authed render call as `nav_available`.
|
|
232
228
|
var navAvailable = { returns: !!returns, reviews: !!reviews };
|
|
233
229
|
|
|
234
|
-
try {
|
|
230
|
+
try { b.audit.registerNamespace(AUDIT_NAMESPACE); } catch (_e) { /* idempotent */ }
|
|
235
231
|
|
|
236
232
|
var W = function (auditAction, h) {
|
|
237
233
|
return _wrap(h, { expectedToken: expectedToken, audit: auditAction });
|
|
@@ -288,7 +284,7 @@ function mount(router, deps) {
|
|
|
288
284
|
}
|
|
289
285
|
throw e;
|
|
290
286
|
}
|
|
291
|
-
|
|
287
|
+
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".product.create", outcome: "success", metadata: {} });
|
|
292
288
|
_redirect(res, "/admin/products?created=1");
|
|
293
289
|
},
|
|
294
290
|
));
|
|
@@ -356,7 +352,7 @@ function mount(router, deps) {
|
|
|
356
352
|
// failure must NOT be reported as success — let it surface.
|
|
357
353
|
try { await op(req.params.id); }
|
|
358
354
|
catch (e) { if (!(e instanceof TypeError)) throw e; }
|
|
359
|
-
|
|
355
|
+
b.audit.safeEmit({ action: AUDIT_NAMESPACE + "." + audit, outcome: "success", metadata: { id: req.params.id } });
|
|
360
356
|
_redirect(res, "/admin/products");
|
|
361
357
|
},
|
|
362
358
|
);
|
|
@@ -495,7 +491,7 @@ function mount(router, deps) {
|
|
|
495
491
|
// smuggle internal data into the bucket.
|
|
496
492
|
var fetched;
|
|
497
493
|
try {
|
|
498
|
-
fetched = await
|
|
494
|
+
fetched = await b.httpClient.request({
|
|
499
495
|
method: "GET",
|
|
500
496
|
url: body.source_url,
|
|
501
497
|
timeoutMs: 20000,
|
|
@@ -528,7 +524,7 @@ function mount(router, deps) {
|
|
|
528
524
|
// declared content-type so the operator can preview the asset
|
|
529
525
|
// without a content-disposition round-trip.
|
|
530
526
|
var ext = _extFromContentType(declared);
|
|
531
|
-
var id =
|
|
527
|
+
var id = b.uuid.v7();
|
|
532
528
|
var key = "media/" + id + (ext ? "." + ext : "");
|
|
533
529
|
try {
|
|
534
530
|
await r2.put(key, buf, body.content_type);
|
|
@@ -686,7 +682,7 @@ function mount(router, deps) {
|
|
|
686
682
|
}
|
|
687
683
|
throw e;
|
|
688
684
|
}
|
|
689
|
-
|
|
685
|
+
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".order.transition", outcome: "success", metadata: { id: id, event: event } });
|
|
690
686
|
_redirect(res, "/admin/orders/" + encodeURIComponent(id) + "?moved=1");
|
|
691
687
|
},
|
|
692
688
|
));
|
|
@@ -699,7 +695,7 @@ function mount(router, deps) {
|
|
|
699
695
|
// "Refund" moves the money first (never a bare state change — that
|
|
700
696
|
// would mark an order refunded with the customer never paid back).
|
|
701
697
|
async function _refundOrder(o, body) {
|
|
702
|
-
var refundIdempotencyKey = "refund:" + o.id + ":" + (body.idempotency_suffix ||
|
|
698
|
+
var refundIdempotencyKey = "refund:" + o.id + ":" + (body.idempotency_suffix || b.uuid.v7());
|
|
703
699
|
var refund = await payment.refund({
|
|
704
700
|
payment_intent: o.payment_intent_id,
|
|
705
701
|
amount_minor: body.amount_minor || undefined,
|
|
@@ -747,7 +743,7 @@ function mount(router, deps) {
|
|
|
747
743
|
// transition only runs after a successful refund).
|
|
748
744
|
return _redirect(res, "/admin/orders/" + encodeURIComponent(id) + "?err=1");
|
|
749
745
|
}
|
|
750
|
-
|
|
746
|
+
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".order.refund", outcome: "success", metadata: { id: id } });
|
|
751
747
|
_redirect(res, "/admin/orders/" + encodeURIComponent(id) + "?moved=1");
|
|
752
748
|
},
|
|
753
749
|
));
|
|
@@ -816,7 +812,7 @@ function mount(router, deps) {
|
|
|
816
812
|
}
|
|
817
813
|
throw e;
|
|
818
814
|
}
|
|
819
|
-
|
|
815
|
+
b.audit.safeEmit({ action: AUDIT_NAMESPACE + "." + auditEvent, outcome: "success", metadata: { id: id } });
|
|
820
816
|
_redirect(res, "/admin/reviews?moved=1");
|
|
821
817
|
});
|
|
822
818
|
}
|
|
@@ -957,7 +953,7 @@ function mount(router, deps) {
|
|
|
957
953
|
}
|
|
958
954
|
throw e;
|
|
959
955
|
}
|
|
960
|
-
|
|
956
|
+
b.audit.safeEmit({ action: AUDIT_NAMESPACE + "." + auditEvent, outcome: "success", metadata: { id: id } });
|
|
961
957
|
_redirect(res, "/admin/returns/" + encodeURIComponent(id) + "?moved=1");
|
|
962
958
|
});
|
|
963
959
|
}
|
|
@@ -1385,11 +1381,11 @@ function mount(router, deps) {
|
|
|
1385
1381
|
else if (values.shop_name.length > 80) notice = "Shop name is too long (max 80 characters).";
|
|
1386
1382
|
else if (values.currency && !/^[A-Z]{3}$/.test(values.currency)) notice = "Currency must be a 3-letter ISO 4217 code (e.g. USD).";
|
|
1387
1383
|
else if (values.contact_email) {
|
|
1388
|
-
var emailReport =
|
|
1384
|
+
var emailReport = b.guardEmail.validate(values.contact_email, { profile: "strict" });
|
|
1389
1385
|
if (!emailReport || emailReport.ok === false) notice = "That contact email doesn't look valid.";
|
|
1390
1386
|
}
|
|
1391
1387
|
if (!notice && values.support_url) {
|
|
1392
|
-
var u =
|
|
1388
|
+
var u = b.safeUrl.parse(values.support_url);
|
|
1393
1389
|
if (!u || (u.protocol !== "https:" && u.protocol !== "http:")) notice = "Support URL must be a valid http(s) URL.";
|
|
1394
1390
|
}
|
|
1395
1391
|
if (notice) {
|
|
@@ -1407,7 +1403,7 @@ function mount(router, deps) {
|
|
|
1407
1403
|
notice: "Couldn't save — " + ((e && e.message) || "please try again."),
|
|
1408
1404
|
}));
|
|
1409
1405
|
}
|
|
1410
|
-
|
|
1406
|
+
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".setup.save", outcome: "success", metadata: {} });
|
|
1411
1407
|
_redirect(res, "/admin/setup?saved=1");
|
|
1412
1408
|
});
|
|
1413
1409
|
}
|
package/lib/affiliates.js
CHANGED
|
@@ -127,20 +127,14 @@ var ALLOWED_UPDATE_COLUMNS = Object.freeze([
|
|
|
127
127
|
"commission_value", "attribution_window_days",
|
|
128
128
|
]);
|
|
129
129
|
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
var bShop;
|
|
134
|
-
function _b() {
|
|
135
|
-
if (!bShop) bShop = require("./index");
|
|
136
|
-
return bShop.framework;
|
|
137
|
-
}
|
|
138
|
-
var C = _b().constants;
|
|
130
|
+
// Framework handle (the vendored blamejs); index.js re-exports this as .framework.
|
|
131
|
+
var b = require("./vendor/blamejs");
|
|
132
|
+
var C = b.constants;
|
|
139
133
|
|
|
140
134
|
// ---- validators ---------------------------------------------------------
|
|
141
135
|
|
|
142
136
|
function _uuid(s, label) {
|
|
143
|
-
try { return
|
|
137
|
+
try { return b.guardUuid.sanitize(s, { profile: "strict" }); }
|
|
144
138
|
catch (e) { throw new TypeError("affiliates: " + label + " — " + (e && e.message || "invalid UUID")); }
|
|
145
139
|
}
|
|
146
140
|
|
|
@@ -320,7 +314,7 @@ function _normalizeEmail(input) {
|
|
|
320
314
|
if (typeof input !== "string" || !input.length) {
|
|
321
315
|
throw new TypeError("affiliates: email must be a non-empty string");
|
|
322
316
|
}
|
|
323
|
-
var guardEmail =
|
|
317
|
+
var guardEmail = b.guardEmail;
|
|
324
318
|
var report;
|
|
325
319
|
try {
|
|
326
320
|
report = guardEmail.validate(input, { profile: "strict" });
|
|
@@ -345,7 +339,7 @@ function _now() { return Date.now(); }
|
|
|
345
339
|
// ---- code generation + canonicalization ---------------------------------
|
|
346
340
|
|
|
347
341
|
function _generateCode() {
|
|
348
|
-
var buf =
|
|
342
|
+
var buf = b.crypto.generateBytes(CODE_LEN);
|
|
349
343
|
var out = "";
|
|
350
344
|
for (var j = 0; j < CODE_LEN; j += 1) {
|
|
351
345
|
out += CODE_ALPHABET.charAt(buf[j] & 31);
|
|
@@ -390,7 +384,7 @@ function create(opts) {
|
|
|
390
384
|
opts = opts || {};
|
|
391
385
|
var query = opts.query;
|
|
392
386
|
if (!query) {
|
|
393
|
-
query = function (sql, params) { return
|
|
387
|
+
query = function (sql, params) { return b.externalDb.query(sql, params); };
|
|
394
388
|
}
|
|
395
389
|
|
|
396
390
|
// Pagination cursors are HMAC-tagged via b.pagination so a caller
|
|
@@ -413,7 +407,7 @@ function create(opts) {
|
|
|
413
407
|
throw new TypeError("affiliates." + label + ": cursor must be an opaque string or null");
|
|
414
408
|
}
|
|
415
409
|
try {
|
|
416
|
-
var state =
|
|
410
|
+
var state = b.pagination.decodeCursor(cursor, cursorSecret);
|
|
417
411
|
if (JSON.stringify(state.orderKey) !== JSON.stringify(LIST_ORDER_KEY)) {
|
|
418
412
|
throw new TypeError("affiliates." + label + ": cursor orderKey mismatch");
|
|
419
413
|
}
|
|
@@ -427,7 +421,7 @@ function create(opts) {
|
|
|
427
421
|
function _encodeNext(rows, limit) {
|
|
428
422
|
var last = rows[rows.length - 1];
|
|
429
423
|
if (!last || rows.length < limit) return null;
|
|
430
|
-
return
|
|
424
|
+
return b.pagination.encodeCursor({
|
|
431
425
|
orderKey: LIST_ORDER_KEY,
|
|
432
426
|
vals: [last.occurred_at, last.id],
|
|
433
427
|
forward: true,
|
|
@@ -435,11 +429,11 @@ function create(opts) {
|
|
|
435
429
|
}
|
|
436
430
|
|
|
437
431
|
function _hashEmail(canonicalEmail) {
|
|
438
|
-
return
|
|
432
|
+
return b.crypto.namespaceHash(EMAIL_NAMESPACE, canonicalEmail);
|
|
439
433
|
}
|
|
440
434
|
|
|
441
435
|
function _hashSession(sessionId) {
|
|
442
|
-
return
|
|
436
|
+
return b.crypto.namespaceHash(SESSION_NAMESPACE, sessionId);
|
|
443
437
|
}
|
|
444
438
|
|
|
445
439
|
async function _getAffiliateRaw(id) {
|
|
@@ -519,7 +513,7 @@ function create(opts) {
|
|
|
519
513
|
var commissionValue = _commissionValue(input.commission_value, commissionKind);
|
|
520
514
|
var attributionDays = _attributionWindow(input.attribution_window_days);
|
|
521
515
|
|
|
522
|
-
var id =
|
|
516
|
+
var id = b.uuid.v7();
|
|
523
517
|
var ts = _now();
|
|
524
518
|
var row = {
|
|
525
519
|
id: id,
|
|
@@ -697,7 +691,7 @@ function create(opts) {
|
|
|
697
691
|
};
|
|
698
692
|
}
|
|
699
693
|
|
|
700
|
-
var visitId =
|
|
694
|
+
var visitId = b.uuid.v7();
|
|
701
695
|
await query(
|
|
702
696
|
"INSERT INTO affiliate_visits " +
|
|
703
697
|
"(id, code, affiliate_id, visitor_session_id_hash, referrer, occurred_at) " +
|
|
@@ -801,7 +795,7 @@ function create(opts) {
|
|
|
801
795
|
aff.commission_kind, Number(aff.commission_value), orderTotal
|
|
802
796
|
);
|
|
803
797
|
|
|
804
|
-
var id =
|
|
798
|
+
var id = b.uuid.v7();
|
|
805
799
|
await query(
|
|
806
800
|
"INSERT INTO affiliate_commissions " +
|
|
807
801
|
"(id, order_id, affiliate_id, order_total_minor, commission_minor, " +
|
|
@@ -848,11 +842,11 @@ function create(opts) {
|
|
|
848
842
|
idx += 1;
|
|
849
843
|
}
|
|
850
844
|
if (cursorVals) {
|
|
851
|
-
var
|
|
852
|
-
var
|
|
845
|
+
var pOccurred = idx;
|
|
846
|
+
var pId = idx + 1;
|
|
853
847
|
where.push(
|
|
854
|
-
"(occurred_at < ?" +
|
|
855
|
-
"(occurred_at = ?" +
|
|
848
|
+
"(occurred_at < ?" + pOccurred + " OR " +
|
|
849
|
+
"(occurred_at = ?" + pOccurred + " AND id < ?" + pId + "))"
|
|
856
850
|
);
|
|
857
851
|
params.push(cursorVals[0], cursorVals[1]);
|
|
858
852
|
idx += 2;
|
package/lib/analytics.js
CHANGED
|
@@ -87,12 +87,8 @@
|
|
|
87
87
|
* discipline owns what goes inside.
|
|
88
88
|
*/
|
|
89
89
|
|
|
90
|
-
var
|
|
91
|
-
|
|
92
|
-
if (!bShop) bShop = require("./index");
|
|
93
|
-
return bShop.framework;
|
|
94
|
-
}
|
|
95
|
-
var C = _b().constants;
|
|
90
|
+
var b = require("./vendor/blamejs");
|
|
91
|
+
var C = b.constants;
|
|
96
92
|
|
|
97
93
|
var ONE_YEAR_MS = C.TIME.days(365);
|
|
98
94
|
var DEFAULT_WINDOW_MS = C.TIME.days(30);
|
|
@@ -231,7 +227,7 @@ function create(opts) {
|
|
|
231
227
|
opts = opts || {};
|
|
232
228
|
var query = opts.query;
|
|
233
229
|
if (!query) {
|
|
234
|
-
query = function (sql, params) { return
|
|
230
|
+
query = function (sql, params) { return b.externalDb.query(sql, params); };
|
|
235
231
|
}
|
|
236
232
|
|
|
237
233
|
// Status buckets we always surface, even when zero. Aligned with
|
|
@@ -470,7 +466,6 @@ function create(opts) {
|
|
|
470
466
|
occurredAt = input.occurred_at;
|
|
471
467
|
}
|
|
472
468
|
|
|
473
|
-
var b = _b();
|
|
474
469
|
var sessionHash = sessionId == null ? null : b.crypto.namespaceHash(SESSION_NAMESPACE, sessionId);
|
|
475
470
|
var customerHash = customerId == null ? null : b.crypto.namespaceHash(CUSTOMER_NAMESPACE, customerId);
|
|
476
471
|
// `session_id_hash` is NOT NULL in the schema — fall back to
|
|
@@ -593,7 +588,7 @@ function create(opts) {
|
|
|
593
588
|
_refuseRawPii("session_id", sessionId);
|
|
594
589
|
var limit = (opts && opts.limit) == null ? 100 : opts.limit;
|
|
595
590
|
_limit(limit, "limit", 500);
|
|
596
|
-
var hash =
|
|
591
|
+
var hash = b.crypto.namespaceHash(SESSION_NAMESPACE, sessionId);
|
|
597
592
|
var r = await query(
|
|
598
593
|
"SELECT id, event_type, session_id_hash, customer_id_hash, " +
|
|
599
594
|
" payload_json, product_id, search_q, page_url, " +
|
package/lib/announcement-bar.js
CHANGED
|
@@ -137,11 +137,7 @@ var ALLOWED_PATCH_COLUMNS = Object.freeze([
|
|
|
137
137
|
"dismissible",
|
|
138
138
|
]);
|
|
139
139
|
|
|
140
|
-
var
|
|
141
|
-
function _b() {
|
|
142
|
-
if (!bShop) bShop = require("./index");
|
|
143
|
-
return bShop.framework;
|
|
144
|
-
}
|
|
140
|
+
var b = require("./vendor/blamejs");
|
|
145
141
|
|
|
146
142
|
// ---- monotonic clock ----------------------------------------------------
|
|
147
143
|
//
|
|
@@ -275,7 +271,7 @@ function _linkUrl(s) {
|
|
|
275
271
|
return s;
|
|
276
272
|
}
|
|
277
273
|
try {
|
|
278
|
-
|
|
274
|
+
b.safeUrl.parse(s, { allowedProtocols: ["https:"] });
|
|
279
275
|
} catch (e) {
|
|
280
276
|
throw new TypeError("announcementBar: link_url — " + (e && e.message || "must be https:// or a /-rooted absolute path"));
|
|
281
277
|
}
|
|
@@ -323,7 +319,7 @@ function create(opts) {
|
|
|
323
319
|
opts = opts || {};
|
|
324
320
|
var query = opts.query;
|
|
325
321
|
if (!query) {
|
|
326
|
-
query = function (sql, params) { return
|
|
322
|
+
query = function (sql, params) { return b.externalDb.query(sql, params); };
|
|
327
323
|
}
|
|
328
324
|
// customerSegments is optional — announcements with audience =
|
|
329
325
|
// "segment" require it, but a deployment without segment-targeted
|
|
@@ -564,7 +560,7 @@ function create(opts) {
|
|
|
564
560
|
var sessionHash = null;
|
|
565
561
|
if (input.session_id != null) {
|
|
566
562
|
var sid = _sessionId(input.session_id);
|
|
567
|
-
sessionHash =
|
|
563
|
+
sessionHash = b.crypto.namespaceHash(SESSION_HASH_NAMESPACE, sid);
|
|
568
564
|
}
|
|
569
565
|
|
|
570
566
|
// Pull every candidate row in the active window, ordered by
|
|
@@ -655,14 +651,14 @@ function create(opts) {
|
|
|
655
651
|
throw new TypeError("announcementBar.recordDismissal: slug " + JSON.stringify(slug) + " not found");
|
|
656
652
|
}
|
|
657
653
|
|
|
658
|
-
var hash =
|
|
654
|
+
var hash = b.crypto.namespaceHash(SESSION_HASH_NAMESPACE, sessionId);
|
|
659
655
|
|
|
660
656
|
// Dedup is enforced by the UNIQUE(announcement_slug,
|
|
661
657
|
// session_id_hash) index; a second dismissal from the same
|
|
662
658
|
// session is a no-op (the existing row keeps its occurred_at).
|
|
663
659
|
// INSERT OR IGNORE keeps the call idempotent without a separate
|
|
664
660
|
// SELECT-then-INSERT round-trip.
|
|
665
|
-
var id =
|
|
661
|
+
var id = b.uuid.v7();
|
|
666
662
|
var r = await query(
|
|
667
663
|
"INSERT OR IGNORE INTO announcement_dismissals " +
|
|
668
664
|
"(id, announcement_slug, session_id_hash, occurred_at) " +
|
|
@@ -695,7 +691,7 @@ function create(opts) {
|
|
|
695
691
|
throw new TypeError("announcementBar.renderHtml: locale must be a BCP-47-shape string (e.g. 'en-US')");
|
|
696
692
|
}
|
|
697
693
|
}
|
|
698
|
-
var escapeHtml =
|
|
694
|
+
var escapeHtml = b.template.escapeHtml;
|
|
699
695
|
|
|
700
696
|
var theme = escapeHtml(announcement.theme || "info");
|
|
701
697
|
var slug = escapeHtml(announcement.slug);
|
package/lib/api-keys.js
CHANGED
|
@@ -76,7 +76,10 @@ var TOKEN_BYTE_LEN = 32;
|
|
|
76
76
|
var TOKEN_PLAINTEXT_LEN = 43;
|
|
77
77
|
var TOKEN_PLAINTEXT_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
// Framework handle (the vendored blamejs); index.js re-exports this as .framework.
|
|
80
|
+
var b = require("./vendor/blamejs");
|
|
81
|
+
|
|
82
|
+
var ROTATION_GRACE_MS = b.constants.TIME.days(1);
|
|
80
83
|
|
|
81
84
|
var OWNER_TYPES = ["operator", "app", "affiliate", "tenant"];
|
|
82
85
|
var STATUSES = ["active", "rotated", "revoked", "expired"];
|
|
@@ -112,19 +115,10 @@ var ALLOWED_UPDATE_COLUMNS = Object.freeze([
|
|
|
112
115
|
"name", "scopes", "rate_limit_per_minute",
|
|
113
116
|
]);
|
|
114
117
|
|
|
115
|
-
// Lazy framework handle — matches the pattern used by every other
|
|
116
|
-
// shop primitive; avoids the require cycle that would arise from
|
|
117
|
-
// importing `./index` at module-eval time.
|
|
118
|
-
var bShop;
|
|
119
|
-
function _b() {
|
|
120
|
-
if (!bShop) bShop = require("./index");
|
|
121
|
-
return bShop.framework;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
118
|
// ---- validators ---------------------------------------------------------
|
|
125
119
|
|
|
126
120
|
function _uuid(s, label) {
|
|
127
|
-
try { return
|
|
121
|
+
try { return b.guardUuid.sanitize(s, { profile: "strict" }); }
|
|
128
122
|
catch (e) { throw new TypeError("apiKeys: " + label + " — " + (e && e.message || "invalid UUID")); }
|
|
129
123
|
}
|
|
130
124
|
|
|
@@ -257,8 +251,8 @@ function _now() { return Date.now(); }
|
|
|
257
251
|
// built-in `base64url` encoding runs in place of a polynomial-ReDoS-
|
|
258
252
|
// shaped `.replace(/=+$/, "")` strip (CodeQL js/polynomial-redos).
|
|
259
253
|
function _generateToken() {
|
|
260
|
-
var buf =
|
|
261
|
-
return
|
|
254
|
+
var buf = b.crypto.generateBytes(TOKEN_BYTE_LEN);
|
|
255
|
+
return b.crypto.toBase64Url(buf);
|
|
262
256
|
}
|
|
263
257
|
|
|
264
258
|
function _canonicalToken(input) {
|
|
@@ -272,7 +266,7 @@ function _canonicalToken(input) {
|
|
|
272
266
|
}
|
|
273
267
|
|
|
274
268
|
function _hashToken(canonical) {
|
|
275
|
-
return
|
|
269
|
+
return b.crypto.namespaceHash(TOKEN_NAMESPACE, canonical);
|
|
276
270
|
}
|
|
277
271
|
|
|
278
272
|
// ---- factory ------------------------------------------------------------
|
|
@@ -281,7 +275,7 @@ function create(opts) {
|
|
|
281
275
|
opts = opts || {};
|
|
282
276
|
var query = opts.query;
|
|
283
277
|
if (!query) {
|
|
284
|
-
query = function (sql, params) { return
|
|
278
|
+
query = function (sql, params) { return b.externalDb.query(sql, params); };
|
|
285
279
|
}
|
|
286
280
|
|
|
287
281
|
async function _getRaw(id) {
|
|
@@ -398,7 +392,7 @@ function create(opts) {
|
|
|
398
392
|
var rateLimit = _rateLimit(input.rate_limit_per_minute);
|
|
399
393
|
var expiresAt = _expiresAt(input.expires_at);
|
|
400
394
|
|
|
401
|
-
var id =
|
|
395
|
+
var id = b.uuid.v7();
|
|
402
396
|
var plaintext = _generateToken();
|
|
403
397
|
var hash = _hashToken(plaintext);
|
|
404
398
|
var ts = _now();
|
|
@@ -468,9 +462,9 @@ function create(opts) {
|
|
|
468
462
|
var row = r.rows[0];
|
|
469
463
|
|
|
470
464
|
// Constant-time equality on whichever column matched.
|
|
471
|
-
var matchedLive =
|
|
465
|
+
var matchedLive = b.crypto.timingSafeEqual(row.token_hash, hash);
|
|
472
466
|
var matchedPrevious = row.token_hash_previous != null
|
|
473
|
-
&&
|
|
467
|
+
&& b.crypto.timingSafeEqual(row.token_hash_previous, hash);
|
|
474
468
|
if (!matchedLive && !matchedPrevious) return null;
|
|
475
469
|
|
|
476
470
|
// expires_at takes precedence over everything else. A key whose
|
|
@@ -561,7 +555,7 @@ function create(opts) {
|
|
|
561
555
|
// row can claim the UNIQUE column without colliding. We use a
|
|
562
556
|
// placeholder hash (the row id under a rotate-marker namespace)
|
|
563
557
|
// so the UNIQUE constraint stays honoured.
|
|
564
|
-
var placeholderHash =
|
|
558
|
+
var placeholderHash = b.crypto.namespaceHash(
|
|
565
559
|
"api-key-rotated-placeholder", current.id + ":" + ts
|
|
566
560
|
);
|
|
567
561
|
await query(
|
|
@@ -572,7 +566,7 @@ function create(opts) {
|
|
|
572
566
|
|
|
573
567
|
// Issue the replacement row. Scopes / rate-limit / expiry /
|
|
574
568
|
// owner all carry forward verbatim — rotation is plaintext-only.
|
|
575
|
-
var newId =
|
|
569
|
+
var newId = b.uuid.v7();
|
|
576
570
|
var plaintext = _generateToken();
|
|
577
571
|
var newHash = _hashToken(plaintext);
|
|
578
572
|
var row = {
|
|
@@ -704,7 +698,7 @@ function create(opts) {
|
|
|
704
698
|
throw miss;
|
|
705
699
|
}
|
|
706
700
|
|
|
707
|
-
var rowId =
|
|
701
|
+
var rowId = b.uuid.v7();
|
|
708
702
|
await query(
|
|
709
703
|
"INSERT INTO api_key_usage (id, key_id, endpoint, occurred_at) " +
|
|
710
704
|
"VALUES (?1, ?2, ?3, ?4)",
|