@bitcoinerrorlog/pubky-shop

中文文档
0.1.7发布于 2026/10/16MITTypeScriptES Module近 30 天下载 3453

Typed Pubky Shop inventory SDK, deterministic import/export codecs, and seller CLI

原始使用文档
文档目录11 节
  1. Package exports
  2. Credential ownership
  3. Inventory client
  4. Deterministic JSON
  5. RFC 4180 CSV
  6. Durable import planning
  7. Safe errors and limits
  8. Seller CLI
  9. Webhooks
  10. Shopify bridge
  11. Development gates

@bitcoinerrorlog/pubky-shop

TypeScript SDK for the Pubky Marketplace inventory HTTP contract, deterministic JSON/CSV interchange, and durable import planning, plus the pubky-shop seller CLI and a Shopify bridge. The package is ESM-only and requires Node 22 or newer. It is published to npm as @bitcoinerrorlog/pubky-shop (latest is 0.1.6) by the publish.yml workflow when a GitHub release is published.

npm install @bitcoinerrorlog/pubky-shop
npx pubky-shop --help

The service it talks to is pubky/pubky-marketplace-service; its HTTP routes are documented in docs/api.md.

Package exports

  • . is the browser-safe entry: codecs, PubkyShopClient (Wave 1 inventory and Wave 3a HTTP), and the in-memory import planner (planImport, planImportStream, MemoryManifestStore, resumeTasks, chunked sync-many helpers). It hashes with Web Crypto-compatible SHA-256 and never imports node: or @synonymdev/pubky.
  • ./node is the Node spool planner: FileManifestStore, disk-backed planImport / planImportStream, resumeTasks, and streamResumeTasks.
  • @synonymdev/pubky is an optional peer used only by the CLI / ./node.
import {
  MemoryManifestStore,
  PubkyShopClient,
  planImport,
} from "@bitcoinerrorlog/pubky-shop";
import { FileManifestStore, planImport as planImportNode } from "@bitcoinerrorlog/pubky-shop/node";

Credential ownership

import { PubkyShopClient } from "@bitcoinerrorlog/pubky-shop";

const client = new PubkyShopClient({
  session: process.env.PUBKY_MARKETPLACE_SESSION!,
  serviceUrl: "https://marketplace.example/",
});

serviceUrl is the marketplace service origin (production: https://marketplace-service-production-ce23.up.railway.app). Every method returns an SdkResult<T>, { ok: true, value } or { ok: false, error }, and does not throw for service or transport failures:

const result = await client.listings(sellerPubky, { limit: 50 });
if (!result.ok) {
  console.error(result.error.code); // for example "session_rejected"
} else {
  console.log(result.value);
}

The host obtains the opaque service bearer out of band, owns persistence, expiry policy, renewal, revocation, account binding, and sign-out cleanup. The SDK:

  • sends Authorization: Bearer only to the validated HTTPS service origin;
  • rejects userinfo, query, fragment, non-root path, and non-HTTPS service URLs;
  • never follows redirects and never silently refreshes a rejected session;
  • returns a static session_rejected error for HTTP 401;
  • never logs, serializes, persists, returns, or hashes the bearer;
  • never accepts a seller secret key, seed, root key, or keypair.

ServiceAuthTokenSigner is an interface-only future host boundary. It returns host-approved AuthToken postcard bytes and the expected pubky. This package does not implement Ring, key storage, homeserver session minting, AuthToken exchange, or automatic bearer renewal. A service AuthToken/bearer is separate from any homeserver credential.

Inventory client

getInventoryProjection(aggregateId) and adjustInventory(request) return SdkResult<T>. They decode the generated Wave 1 contract while preserving unknown response fields for forward compatibility. The client binds fetch to the global object so Chromium Window.fetch is not invoked as a method. Wave 3a seller HTTP is on the same class: listings, orders, events, getListing, syncMany (chunks of 100), webhooks, and sessions. createSession POSTs AuthToken bytes as application/octet-stream and does not attach an existing bearer. Wave 1 Rust i64 fields (server_revision and every stock quantity) are exposed consistently as bigint, including ordinary small values. Integer JSON tokens are captured before JavaScript Number conversion, values outside the signed 64-bit range and non-integer tokens fail with invalid_response, and unknown integer fields are also retained losslessly as bigint.

InventoryAdjustRequest.expected_revision and delta are bigint. The SDK serializes them as exact unquoted JSON integer tokens; for example expected_revision: 9007199254740992n sends "expected_revision":9007199254740992. Response bodies remain bounded, server messages are never reflected, and only allowlisted service error codes are exposed.

The projection's stock.authority is listing_total. On a variant-tracked listing the projection also carries variants, one row per size (variant_id, optional sku, enabled, total, available, reserved, sold, sold_out), decoded and validated.

InventoryAdjustRequest.variant ({ id?, sku? }) selects a size and is required by the service on a variant-tracked listing. Its answer carries stock.authority: "variant" (VariantStockView: the size's counters plus the listing sum in listing_total); a single-variant listing still answers listing_total. Narrow on stock.authority before reading variant_id.

MethodService route
getInventoryProjection(aggregateId)GET /v1/inventory/listings/{aggregateId}
adjustInventory(request)POST /v1/inventory/adjust
listings(pubky, query?)GET /v1/sellers/{pubky}/listings
orders(pubky, query?)GET /v1/sellers/{pubky}/orders
events(pubky, query?)GET /v1/sellers/{pubky}/events
getListing(seller, listingId)GET /v1/listings/{seller}/{listingId}
syncMany(listings)POST /v1/listings/sync-many (chunks of 100)
addWebhook(url)POST /v1/webhooks
rotateWebhook(id)POST /v1/webhooks/{id}/rotate
deleteWebhook(id)DELETE /v1/webhooks/{id}
listSessions()GET /v1/auth/sessions
createSession(authToken)POST /v1/auth/sessions
updateSession(id, patch)PATCH /v1/auth/sessions/{id}
revokeSession(id)DELETE /v1/auth/sessions/{id}

The pinned executable contract is test/fixtures/service/inventory.json, copied byte-for-byte from pubky/pubky-marketplace-service revision 4d5c07c0f273616c4fba06697f90d4baef5a7722, migration 0034. See the fixture provenance file for source and SHA-256 details.

Deterministic JSON

  • canonicalJson implements RFC 8785 JCS text.
  • encodeCanonicalJson emits UTF-8 with exactly one trailing LF.
  • parseBoundedJson rejects invalid UTF-8, duplicate object names, excessive bytes/depth/nodes/string size, and unsupported Unicode.
  • decodeExportEnvelope / encodeExportEnvelope preserve unknown envelope, listing-wrapper, and projection fields while keeping service projection fields out of seller-authored records.
  • captureSignedRecord retains exact signed-record bytes and a SHA-256 sidecar. emitSignedRecord(capture) is byte-identical for unchanged records. A changed record requires a host-injected production schema validator and is then JCS serialized. Validation refusal performs no write.

The JSON codec never merges service projections into signed records.

RFC 4180 CSV

exportCanonicalCsv(rows, { excelBom?: boolean }) writes a mandatory header, one row per variant, quoted UTF-8 cells, CRLF records, deterministic columns and row ordering, and canonical JSON nested cells. Normal output has no BOM; the explicit Excel option emits one. Dangerous spreadsheet prefixes (=, +, -, @) and leading apostrophes use a reversible apostrophe escape.

parseCanonicalCsvStream(source, onRow, limits) is the unbounded-catalog byte API. source may be an AsyncIterable<Uint8Array>, a ReadableStream<Uint8Array>, or a synchronous iterable. It incrementally hashes source bytes, decodes UTF-8, recognizes a BOM even when its three bytes arrive separately, and preserves RFC 4180 state across chunk-split CRLF, escaped quotes, and multibyte characters. It retains only the current bounded row/cell and reports measured peak parser-owned bytes. Cross-row duplicate and listing-group validation is performed by planImportStream before commit.

parseCanonicalCsv(bytes) is the bounded, materializing convenience codec. It accepts BOM or no BOM and rejects lone LF/CR, truncated quotes, duplicate headers/rows/variant ids, ambiguous SKUs, conflicting listing fields, raw formula payloads, invalid identity/mapping, unsafe integers, and over-limit input. Unknown extra columns are retained in extraFields and exported deterministically.

Canonical columns are:

record_uri, seller_pubky, listing_id, source_listing_key, record_revision, variant_id, sku, state, title, description, taxonomy_json, category, condition, tags_json, amount_minor, currency, exponent, variant_quantity, variant_enabled, options_json, media_json, shipping_options_json, return_policy_json, sale_json, and external_refs_json.

Identity comes only from record_uri, (seller_pubky, listing_id), or an explicit source_listing_key for a new record. Title is never identity. Streaming input has no fixed total-byte or total-row cap. Row bytes, cell bytes, columns, nested JSON depth, and parser/planner working sets are bounded and fail with typed safe observed values. The byte-array convenience parser and exporter additionally retain explicit total byte/row caps.

Durable import planning

Browser hosts plan against an in-memory ManifestStore (Shop supplies Dexie):

import {
  MemoryManifestStore,
  browserFileSource,
  planImportStream,
  resumeTasks,
} from "@bitcoinerrorlog/pubky-shop";

const store = new MemoryManifestStore();
const planned = await planImportStream(browserFileSource(file), { store });

planImport / planImportStream on . stream-parse with Web APIs, bound the working set (64 MiB convenience / parser-planner bytes), and call store.create only after EOF, UTF-8/CSV/JSON/mapping validation, identity-index checks, and source SHA-256 completion. Parse failure leaves zero rows. JSON is a bounded convenience path ([{…}] or {rows:[…]}); CSV is streamed. Idempotency keys are UUID v5-like from SHA-256 of schema, manifest id, row identity, and normalized hash — the same bytes as the Node planner. chunkSyncManyListings splits publish batches at 100. classifySyncManyItem reads per-id 207 results.

Node hosts keep the disk spool planner:

import {
  FileManifestStore,
  planImportStream,
  replayImport,
  resumeTasks,
} from "@bitcoinerrorlog/pubky-shop/node";

const store = new FileManifestStore("/secure/host-owned/import-manifests");
const planned = await planImportStream(fileReadable, { store });

Planning incrementally writes a mode-0600 spool and external grouping indexes inside the host-provided manifest directory. Bounded external merge sorts validate duplicate rows, variant identities, SKUs, and listing-level facts without retaining decoded source or all rows in process memory. The first committed manifest write occurs only after EOF, UTF-8/CSV/mapping validation, external grouping, generated-id collision checks, and source SHA-256 completion. Malformed late input removes the planning spool and leaves no manifest.

A manifest fixes its id, exact source SHA-256 and decimal source-byte count, parser/mapping/schema versions, complete immutable row identities, normalized row hashes, actions, generated listing ids, and deterministic idempotency keys. The JSONL manifest is streamed to an fsynced temporary file and atomically renamed. loadSummary and streamRows avoid materialization; load and planImport(bytes, ...) are explicitly bounded convenience APIs. streamRows validates the complete file and trailing row count before it yields any authoritative row. checkpointImportRow performs a bounded streaming atomic rewrite, and streamResumeTasks resumes catalogs of any row count without calling load. Generated ids are persisted before a future publisher may act.

FileManifestStore is the production host-supplied adapter included in this package. It canonicalizes the configured root once, pins its device, inode, and mode, and verifies that identity before and after lock, manifest, spool, quarantine, and cleanup operations. Final opens use no-follow flags where Node exposes them; files are mode 0600, the root and planning directories are mode 0700, and writes use inter-process exclusion, fsync, and atomic rename or append.

This is deliberately not an openat confinement claim: Node has no directory-relative no-follow API. The host configuration and the canonical root's parent are trusted and must not be concurrently renameable by an attacker. Detected root replacement fails closed, but a privileged parent-directory attacker racing the unavoidable check/open gap is outside the adapter's threat model. A symlink in a trusted parent is canonicalized; a symlink as the configured final root or a managed final file is refused. Abandoned dead-process .planning and .tmp artifacts are reaped only after the configured age, while live-PID, young, changed-identity, and unknown files are retained.

Per-row checkpoints are planned, publishing, published_unsynced, complete, conflict, and failed. Restart maps an uncertain publishing row to reconcile_publish, never a blind repeat; published_unsynced resumes only the service-sync leg.

replayImport returns the existing manifest for identical row identities and hashes. Changed content is recorded in a separate immutable pubky-shop-replay-quarantine JSONL log with its own monotonic version and a compare-and-append check against the manifest version. Identity is SHA-256(manifest schema/version, replay source SHA-256, canonical conflict hash): sequential, concurrent, and post-restart duplicate replays return the existing durable record without adding a version. Metadata records are bounded and appended with O_APPEND; conflict rows live in an immutable, content-verified JSONL sidecar and are read with streamReplayConflicts. Neither append nor read rewrites or materializes prior history.

D6.19 retention is host-operated: retain manifest/result and quarantine files for at least 30 days, then call compactReplayQuarantines with the retention cutoff. Compaction is the explicit bounded streaming maintenance operation; after an identity ages out, a later replay may create a new audit record. Storage is therefore bounded by the host's retained replay volume, not by an unsafe in-process cap. A truncated quarantine tail fails closed until the host calls repairReplayQuarantineTail, which validates every complete record and removes only the incomplete final fragment. discardManifest is the version-CAS discard control; it makes the manifest unavailable first, then removes its retained quarantine/conflict files. Replay never changes complete, conflict, failed, or any other row checkpoint. Quarantine reports survive a new FileManifestStore instance. Malformed or truncated CSV creates no manifest. The planner itself performs no homeserver PUT, fake compare-and-swap, or service publication; the CLI listings import command and the Shopify bridge drive publication on top of it.

External grouping retains only one fixed fan-in batch. Generated runs use deterministic generation/index names rather than an in-memory path catalog. The byte budget charges each input's fixed 64 KiB stream/readline/decoder overhead, line head, iterator, pathname, and output metadata; fan-in is the minimum allowed by that budget and the explicit descriptor cap (maxSortOpenFiles, default 16). Reported resource usage includes both the configured cap and measured descriptor/metadata/working-set high-water marks. A configuration that cannot support a two-way merge is rejected before reading input.

Safe errors and limits

PubkyShopError uses static messages. Details contain only bounded numbers, field names, allowlisted service codes, manifest ids, and row identities. Bearer values, transport exception text, server messages/bodies, and source cell text are excluded.

Error codes:

invalid_configuration, invalid_service_url, invalid_session, origin_violation, session_rejected, transport_error, response_limit_exceeded, invalid_response, service_error, invalid_json, unsupported_json_value, limit_exceeded, malformed_csv, invalid_csv_header, duplicate_row, duplicate_variant_id, ambiguous_sku, conflicting_listing_fields, formula_payload, invalid_identity, invalid_mapping, validation_failed, unsupported_record_version_or_field, manifest_conflict, manifest_store_error, and changed_replay_quarantined.

Seller CLI

pubky-shop is the seller command-line client. Shipped defaults are production origins (https://shop.pubky.app and https://marketplace-service-production-ce23.up.railway.app). Override with --bff-url / --service-url, PUBKY_SHOP_BFF_URL / PUBKY_SHOP_SERVICE_URL, or $XDG_CONFIG_HOME/pubky-shop/config.json keys bff_url and service_url (flags beat env beat file).

Marketplace login uses Shop BFF CLI grant routes. The CLI generates result_cpk / result_delivery_id, proves an existing homeserver session with a homeserver PoP PUT, and never holds Shop cookies or BFF signing keys. Login binds the grant to the homeserver session pubky. Headless proof (CI and live tests) mints a staging seat, then calls @synonymdev/pubky 0.11 Signer.approveAuthRequest on the pubkyauth://signin_grant URL so the SDK posts GrantClaims. @synonymdev/pubky 0.8 posts an AuthToken onto that inbox and the grant worker terminalizes grant_invalid. After approval, auth login --complete tickets and claims the service bearer. auth login loads a stored homeserver session covering /pub/pubky.app/marketplace/:rw from keychain service pubky-shop, account {origin}|{pubky}|homeserver-session, or from a mode-0600 file in PUBKY_SHOP_CREDENTIAL_DIR / the config credential directory. The payload is {pubky,capabilities,secret} where secret is Session.exportLocalSecret(). Missing that item is exit 2 homeserver_session_missing. Unreadable or unrestorable material is exit 2 homeserver_session_invalid. The marketplace bearer is a separate item, account {origin}|{pubky}. Claim does not return a session id; auth logout requires --force-local unless a UUID session id is present.

PUBKY_SHOP_LIVE=1 npm test -- test/cli.live.test.ts
pubky-shop auth login --json --complete
pubky-shop auth status --json
pubky-shop listings export --format json --output listings.json
pubky-shop listings import --input listings.json
pubky-shop orders export --json
pubky-shop events tail --json
pubky-shop webhook add --url https://example.invalid/hook
pubky-shop webhook rotate --id <webhook-id>
pubky-shop webhook delete --id <webhook-id>
pubky-shop auth logout --force-local

--json prints {ok,data,error{code,message}}. Exit 0 success, 1 usage, 2 auth/denied, 3 remote/rate-limit/unavailable.

Webhooks

Register an endpoint with pubky-shop webhook add --url <https-url> or client.addWebhook(url); rotateWebhook / webhook rotate issues a new secret and key id, and deleteWebhook / webhook delete removes the endpoint. The service signs every delivery with these headers:

HeaderValue
Pubky-Webhook-Version1
Pubky-Webhook-Idevent id (UUID)
Pubky-Webhook-TimestampUnix seconds
Pubky-Webhook-Key-Idkey id (UUID) of the endpoint's current secret
Pubky-Webhook-Signaturev1= plus lowercase hex HMAC-SHA256

The HMAC is computed over v1.{timestamp}.{event id}.{raw body} and keyed by the SHA-256 of the secret's bytes, where the secret is the unpadded base64url string returned when the endpoint is created or rotated. Verify against the raw request bytes, not re-serialized JSON. verifyPubkyWebhook (Node only, exported from the Shopify connector entry) checks the key id, a timestamp skew, the signature format and the signature in constant time:

import { verifyPubkyWebhook } from "@bitcoinerrorlog/pubky-shop/connectors/shopify";

const verdict = verifyPubkyWebhook({
  raw: rawBodyBytes, // Uint8Array of the exact request body
  secretBase64Url: process.env.PUBKY_WEBHOOK_SECRET!,
  expectedKeyId: process.env.PUBKY_WEBHOOK_KEY_ID!,
  eventId: headers["pubky-webhook-id"],
  timestamp: headers["pubky-webhook-timestamp"],
  keyId: headers["pubky-webhook-key-id"],
  signature: headers["pubky-webhook-signature"],
  nowSeconds: Math.floor(Date.now() / 1000),
  maxSkewSeconds: 300,
});
if (!verdict.ok) {
  // verdict.reason: "malformed" | "stale" | "wrong_key" | "bad_signature"
}

Deduplicate deliveries on Pubky-Webhook-Id.

Shopify bridge

@bitcoinerrorlog/pubky-shop/connectors/shopify maps current and legacy Shopify product CSV headers, and the products/update / inventory_levels/update webhook bodies, onto canonical CSV rows, then drives the existing client: homeserver PUT, syncMany, inventory.adjust with external_ref.channel shopify, seller events, and Pubky webhook verification. CSV quantity columns are recorded as losses. map-csv does not write them as canonical stock. The Admin API client speaks GraphQL inventorySetQuantities with compare-quantity. Tests run it against recorded responses. The live store, token, and webhook endpoint are documented in connectors/shopify-bridge/BOUNDARY.md.

shopify-bridge map-csv --input products.csv --seller <pubky> --currency USD --exponent 2 --output canonical.csv --losses losses.json
shopify-bridge config-summary --secrets bridge-secrets.json
shopify-bridge webhook --secrets bridge-secrets.json --receipts ./receipts --topic products/update --shop-domain shop.myshopify.com --webhook-id <id> --event-id <id> --triggered-at <iso> --hmac <base64>

webhook restores the stored homeserver session (PUBKY_SHOP_PUBKY and the CLI credential store) and writes listing bytes with storage.putBytes. --put-dir is refused. The secret file holds the Admin token and the Pubky marketplace session. config-summary prints the shop domain and seller pubky only. The . export does not import this Node-only entry.

Development gates

Runtime dependencies (@noble/curves, qrcode) belong to the CLI. @synonymdev/pubky is an optional peer of ./node and the CLI. The . export does not import them.

npm run format:check
npm run lint
npm run typecheck
npm test
npm run test:full
npm run build
npm run consumer:check
npm run browser:check
npm run pack:dry
npm run gate
npm run gate:full

npm test / npm run gate keep the 10k-row catalog that still spills to disk. GATE_FULL=1 npm test (or npm run test:full / npm run gate:full) adds the 100001-row catalog. consumer:check and browser:check import the built package the way a consumer would. npm run pack:dry validates package contents without publishing.

版本与发布记录

@bitcoinerrorlog/pubky-shop 共发布 8 个版本,首次发布于 2026/09/21;过去 12 个月发布了 8 个版本。

版本发布日期距今
0.1.7latest2026/10/1623 天前
0.1.62026/09/281 个月前
0.1.52026/09/231 个月前
0.1.42026/09/221 个月前
0.1.32026/09/221 个月前
0.1.22026/09/211 个月前

关于 @bitcoinerrorlog/pubky-shop 的常见问题

@bitcoinerrorlog/pubky-shop 是什么?

Typed Pubky Shop inventory SDK, deterministic import/export codecs, and seller CLI

如何安装 @bitcoinerrorlog/pubky-shop?

在项目目录运行 npm install @bitcoinerrorlog/pubky-shop,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 pubky-shop、shopify-bridge,可以用 npx pubky-shop 直接运行。

@bitcoinerrorlog/pubky-shop 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@bitcoinerrorlog/pubky-shop 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@bitcoinerrorlog/pubky-shop 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@bitcoinerrorlog/pubky-shop 可以商用吗?

@bitcoinerrorlog/pubky-shop 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@bitcoinerrorlog/pubky-shop 的下载量有多少?

根据 npm 官方下载统计,@bitcoinerrorlog/pubky-shop 在 2026/10/08 – 2026/11/06 这 30 天内被下载 3,453 次,日均约 115 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@bitcoinerrorlog/pubky-shop 还在维护吗?

最新版本 0.1.7 发布于 2026/10/16(23 天前);至今共发布 8 个版本,其中过去 12 个月发布了 8 个;首次发布于 2026/09/21。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。