Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ your own 32-byte base64url key.

Verified browser-facing defaults are:

- Lock Server: <http://localhost:3000>
- creator demo: <http://localhost:8080/examples/js-sdk/>
- reader demo: <http://localhost:8081/reader/>
- Lock Server: <http://127.0.0.1:3000>
- creator demo: <http://127.0.0.1:8080/examples/js-sdk/>
- reader demo: <http://127.0.0.1:8081/reader/>

For the opt-in payment-lock demonstration, including Paykit Server, Bitcoin regtest,
and Fulcrum, use the separate Compose definition:
Expand All @@ -65,8 +65,8 @@ docker compose -f compose.paykit-local-demo.yaml up --build

Its external build contexts use anonymously reachable public repositories pinned to
immutable commits; no sibling Paykit or Pubky checkout is required. The full demo adds
Paykit Server at <http://localhost:3001> and publishes the reader at
<http://localhost:8088/reader/>. Pubky Testnet is built from `pubky/pubky-core` source at
Paykit Server at <http://127.0.0.1:3001> and publishes the reader at
<http://127.0.0.1:8088/reader/>. Pubky Testnet is built from `pubky/pubky-core` source at
commit `75eb1324f86e8caa16c41f18a2cd6b8e1909ee7b`, not from a released Pubky image or
version. Payment remains a manual operator action.

Expand Down Expand Up @@ -439,7 +439,8 @@ Example:
"params": {
"recipient_pubky": "pubky<creator_z32>",
"amount": "50000",
"asset": "BTC"
"asset": "BTC",
"payment_in": 24
}
}
],
Expand Down Expand Up @@ -472,7 +473,7 @@ For example:

A submitted proof bundle is sent by the viewer before verification. It is not stored as an entitlement unless verification succeeds.

For `paykit-payment`, the content lock criterion params are exactly `recipient_pubky`, positive base-unit string `amount`, and non-empty `asset`. `recipient_pubky` must equal the content-lock creator. In v1 it must be the lock's only criterion, referenced exactly once by the lock logic. The submitted proof carries no payment details in its proof payload; it uses top-level `reader_public_key` plus the canonical `pubky_lock_resource` so the Lock Server can create the Paykit invoice.
For `paykit-payment`, the content lock criterion params are exactly `recipient_pubky`, positive base-unit string `amount`, non-empty `asset`, and positive whole-hour JSON `u64` `payment_in`. `recipient_pubky` must equal the content-lock creator. In v1 it must be the lock's only criterion, referenced exactly once by the lock logic. The submitted proof carries no payment details in its proof payload; it uses top-level `reader_public_key` plus the canonical `pubky_lock_resource` so the Lock Server can create the Paykit invoice.

Example:

Expand Down
59 changes: 46 additions & 13 deletions compose.paykit-local-demo.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ services:
.local/pubky-homeserver \
.local/locks-server \
.local/paykit-server \
.local/paykit-config
.local/paykit-config \
.local/homegate-bridge
chmod 0600 .local/paykit-server/paykit.env
volumes:
- ./examples/js-sdk:/workspace/examples/js-sdk:ro
Expand Down Expand Up @@ -141,6 +142,8 @@ services:
depends_on:
bitcoin-bootstrap:
condition: service_completed_successfully
ports:
- "127.0.0.1:${LOCKS_ELECTRUM_PORT:-60001}:50001"

electrum-readiness:
image: node:22-bookworm-slim@sha256:813a7480f28fdadac1f7f5c824bcdad435b5bc1322a5968bbbdef8d058f9dff4
Expand All @@ -165,7 +168,6 @@ services:
- "127.0.0.1:${LOCKS_HTTP_RELAY_PORT:-15412}:15412"
- "127.0.0.1:${LOCKS_HOMESERVER_HTTP_PORT:-6286}:6286"
- "127.0.0.1:${LOCKS_HOMESERVER_PUBKY_PORT:-6287}:6287"
- "127.0.0.1:${LOCKS_HOMESERVER_ADMIN_PORT:-6288}:6288"
- "127.0.0.1:${LOCKS_SERVER_PORT:-3000}:3000"
- "127.0.0.1:${LOCKS_PAYKIT_PORT:-3001}:3001"
- "127.0.0.1:${LOCKS_CREATOR_DEMO_PORT:-8080}:8080"
Expand All @@ -177,6 +179,38 @@ services:
postgres:
condition: service_healthy

homegate-bridge:
image: node:22-bookworm-slim@sha256:813a7480f28fdadac1f7f5c824bcdad435b5bc1322a5968bbbdef8d058f9dff4
user: "1000:1000"
working_dir: /workspace
command:
- /bin/sh
- -euc
- |
set -a
. /run/compose-local/homegate-bridge/homegate.env
set +a
exec node examples/js-sdk/scripts/homegate-bridge.mjs
environment:
HOMEGATE_BRIDGE_CONFIG: /run/compose-local/demo-config/config.json
HOMEGATE_BRIDGE_HOMESERVER_ADMIN_URL: http://pubky-testnet:6288
ports:
- "127.0.0.1:${LOCKS_HOMEGATE_PORT:-6288}:8082"
volumes:
- ./examples/js-sdk:/workspace/examples/js-sdk:ro
- ./.local/demo-config:/run/compose-local/demo-config:ro
- ./.local/homegate-bridge:/run/compose-local/homegate-bridge:ro
depends_on:
pubky-testnet:
condition: service_started
demo-config:
condition: service_completed_successfully
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8082/health').then((response) => { if (!response.ok) process.exit(1); }).catch(() => process.exit(1))"]
interval: 2s
timeout: 2s
retries: 30

locks-server:
build:
context: .
Expand Down Expand Up @@ -247,11 +281,11 @@ services:
paykit-server:
image: pubky-locks-paykit-server:local
build:
context: "https://github.com/pubky/paykit-server.git#f38c7915e6b9b104e040773e78438f8aa984c46c"
context: "https://github.com/pubky/paykit-server.git#5ed3e8e849a16045c26c37a75068625dda333785"
dockerfile: Dockerfile.local
additional_contexts:
paykit-lib: "https://github.com/pubky/paykit-rs.git#52a852995bfc457b78d32f5a45f6741766a89bba:paykit-lib"
paykit-sdk: "https://github.com/pubky/paykit-rs.git#52a852995bfc457b78d32f5a45f6741766a89bba:paykit-sdk"
paykit-lib: "https://github.com/pubky/paykit-rs.git#6b241878a9bba5cecea919c0298c3f90624be6ff:paykit-lib"
paykit-sdk: "https://github.com/pubky/paykit-rs.git#6b241878a9bba5cecea919c0298c3f90624be6ff:paykit-sdk"
locks: "https://github.com/pubky/locks.git#df5ea1b6d8dcdec3a9b5a915c3f57bca69d75c8a"

depends_on:
Expand Down Expand Up @@ -320,19 +354,16 @@ services:
PAYKIT_COMPANION_AUTH_BIN: /usr/local/bin/paykit-companion-auth
PUBKY_LOCK_DEBUG: ${PUBKY_LOCK_DEBUG:-0}
volumes:
- ./locks-sdk/bindings/js/pkg:/workspace/locks-sdk/bindings/js/pkg:ro
- ./.local/demo-config:/workspace/.local/demo-config:ro
- ./.local/js-sdk-demo:/workspace/.local/js-sdk-demo
- ./.local/content-creator:/workspace/.local/content-creator
- ./.local/creator-public:/workspace/.local/creator-public
command:
- sh
- -lc
- |
set -eu
npm --prefix examples/js-sdk run create-user -- --role content-creator
npm --prefix examples/js-sdk run publish-creator-profile
npm --prefix examples/js-sdk run start-server -- --allow-unhealthy
rm -f /workspace/.local/creator-public/profile.json
npm --prefix examples/js-sdk run start-server -- --external-wallet

reader-demo:
restart: unless-stopped
Expand Down Expand Up @@ -363,9 +394,9 @@ services:
PAYKIT_READER_RECEIVER_PATH: bitkit/wallet
PAYKIT_READER_SERVER_PATH: bitkit/server
PAYKIT_READER_WORKER_ENABLED: "1"
PAYKIT_EXTERNAL_READER_PUBKY: ${PAYKIT_EXTERNAL_READER_PUBKY:-}
PUBKY_LOCK_DEBUG: ${PUBKY_LOCK_DEBUG:-0}
volumes:
- ./locks-sdk/bindings/js/pkg:/workspace/locks-sdk/bindings/js/pkg:ro
- ./.local/demo-config:/workspace/.local/demo-config:ro
- ./.local/creator-public:/workspace/.local/creator-public:ro
- ./.local/content-viewer:/workspace/.local/content-viewer
Expand All @@ -374,8 +405,10 @@ services:
- sh
- -euc
- |
npm --prefix examples/js-sdk run create-user -- --role content-viewer
exec node examples/js-sdk/scripts/start-reader-demo-server.mjs --allow-unhealthy
if [ -z "$PAYKIT_EXTERNAL_READER_PUBKY" ]; then
npm --prefix examples/js-sdk run create-user -- --role content-viewer
fi
exec node examples/js-sdk/scripts/start-reader-demo-server.mjs
healthcheck:
test:
- CMD
Expand Down
13 changes: 12 additions & 1 deletion docker/js-demo.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
# syntax=docker/dockerfile:1.7@sha256:a57df69d0ea827fb7266491f2813635de6f17269be881f696fbfdf2d83dda33e
FROM paykit-runtime AS paykit-runtime

FROM rust:1.91.1-slim-bookworm@sha256:8514999d4786ef12efe89239e86b3d0a021b94b9d35108c8efe6c79ca7dc1a65 AS locks-sdk-wasm
RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential ca-certificates libssl-dev pkg-config \
&& rm -rf /var/lib/apt/lists/*
RUN rustup target add wasm32-unknown-unknown \
&& cargo install wasm-pack --version 0.13.1 --locked
WORKDIR /workspace
COPY . .
RUN cd locks-sdk/bindings/js && wasm-pack build --target web --out-dir pkg

FROM node:22-bookworm-slim@sha256:813a7480f28fdadac1f7f5c824bcdad435b5bc1322a5968bbbdef8d058f9dff4
WORKDIR /workspace
RUN apt-get update \
Expand All @@ -10,8 +20,9 @@ COPY --chown=node:node examples/js-sdk/package.json examples/js-sdk/package-lock
RUN npm --prefix examples/js-sdk ci --ignore-scripts \
&& npm cache clean --force
COPY --chown=node:node examples/js-sdk /workspace/examples/js-sdk
COPY --from=locks-sdk-wasm --chown=node:node /workspace/locks-sdk/bindings/js/pkg /workspace/locks-sdk/bindings/js/pkg
COPY --from=paykit-runtime /usr/local/bin/paykit-companion-auth /usr/local/bin/paykit-companion-auth
COPY --from=paykit-runtime /usr/local/bin/paykit-reader-demo /usr/local/bin/paykit-reader-demo
RUN mkdir -p /workspace/locks-sdk/bindings/js/pkg /workspace/.local \
RUN mkdir -p /workspace/.local \
&& chown -R node:node /workspace
USER node:node
6 changes: 3 additions & 3 deletions docker/locks-server-compose-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ frontend_session_ttl_seconds = 86400
frontend_session_code_ttl_seconds = 120

[creator_authority_acquisition.legacy_connect]
allowed_return_origins = ["http://localhost:8080"]
allowed_return_origins = ["http://127.0.0.1:8080", "http://localhost:8080"]

[secrets]
creator_authority_key_env = "PUBKY_LOCK_CREATOR_AUTH_ENCRYPTION_KEY"
Expand All @@ -101,8 +101,8 @@ network = "testnet"
public_ip = "127.0.0.1"
public_pubky_tls_port = 6287
public_icann_http_port = 3000
icann_domain = "localhost"
pkarr_relays = ["http://localhost:15411"]
icann_domain = "127.0.0.1"
pkarr_relays = ["http://127.0.0.1:15411"]
key_republisher_interval_seconds = 86400

[rate_limits.verification_submission]
Expand Down
4 changes: 3 additions & 1 deletion docs/ADRs/0020-locks-paykit-v1-integration-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@ The v1 content-lock criterion has verifier wire value `paykit-payment` and param
{
"recipient_pubky": "pubky<creator>",
"amount": "50000",
"asset": "BTC"
"asset": "BTC",
"payment_in": 24
}
```

- `recipient_pubky` must equal the canonical content-lock creator.
- `amount` is a positive decimal integer string in the asset's base unit.
- `asset` is an opaque, non-empty string to Locks. Paykit Server owns deployment-specific asset support and base-unit interpretation.
- `payment_in` is a required, nonzero JSON `u64` number of whole hours in Locks policy.
- V1 permits exactly one payment criterion, referenced exactly once by the lock logic, and exactly one submitted payment proof.
- The submitted payment proof payload is `{}`. `reader_public_key` is top-level submission data.
- Content-lock authoring does not require runtime Paykit configuration or availability.
Expand Down
9 changes: 6 additions & 3 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ The Lock Server has one non-production route family and one authenticated creato
- Can run in `development`, `staging`, or `production`.
- Require `Authorization: Bearer <frontend_session_token>`.
- Derive creator identity from the frontend session. Request-body `creator` is rejected for authenticated routes.
- A guarded path can be owned by only one managed Content Lock for that creator. Creating a different Lock ID for an owned path returns `409 content_lock_path_conflict`.
- Missing/unknown/expired frontend sessions use the JSON error envelope (`401 frontend_session_unavailable` or `401 frontend_session_expired`).
- Missing/revoked creator-granted homeserver authority remains a separate operational error (`503 creator_authority_unavailable`).
- Creator authority status route: `GET /creator/authority-status`
Expand All @@ -46,7 +47,7 @@ Gated-off routes are plain Axum `404 Not Found` responses because the route is i
| --- | --- | --- | --- | --- |
| `PUT /creator/priv-resources/content/<path>` | `200` JSON guarded-resource descriptor | Requires `Authorization: Bearer <frontend_session_token>`. Raw bytes body; MIME from `Content-Type`. | No bearer secrets or raw bytes in response. | `400 invalid_request`, `401 frontend_session_unavailable`, `401 frontend_session_expired`, `413 payload_too_large`, `503 creator_authority_unavailable` |
| `DELETE /creator/priv-resources/content/<path>` | `204` empty response | Requires `Authorization: Bearer <frontend_session_token>`. | No bearer secrets or raw bytes in response. | `401 frontend_session_unavailable`, `401 frontend_session_expired`, `404 guarded_resource_not_found`, `503 creator_authority_unavailable` |
| `POST /creator/content-locks` | `200` JSON content lock | Requires `Authorization: Bearer <frontend_session_token>`. | No bearer secrets in response. | `400 invalid_request`, `404 guarded_resource_not_found`, `401 frontend_session_unavailable`, `401 frontend_session_expired`, `503 creator_authority_unavailable` |
| `POST /creator/content-locks` | `200` JSON content lock | Requires `Authorization: Bearer <frontend_session_token>`. | No bearer secrets in response. | `400 invalid_request`, `401 frontend_session_unavailable`, `401 frontend_session_expired`, `404 guarded_resource_not_found`, `409 content_lock_path_conflict`, `503 creator_authority_unavailable` |
| `POST /creator/lock-service-config` | `200` JSON lock-service pointer | Requires `Authorization: Bearer <frontend_session_token>`. | No bearer secrets in response. | `400 invalid_request`, `401 frontend_session_unavailable`, `401 frontend_session_expired`, `503 creator_authority_unavailable` |
| `GET /connect` | `200` HTML Lock-Server-hosted connect shell | No bearer auth. Mounted when `[creator_authority_acquisition].enabled = true`; `return_to` must match `allowed_return_origins` or explicit wildcard policy. | HTML intentionally contains the secret-bearing Pubky authorization URL on Lock Server origin; response must not contain frontend session token, one-time code, or creator authority secret. | `400 invalid_request`, `503 creator_authority_unavailable`, `404` when route gated off |
| `POST /connect/{flow_id}/complete` | `303` redirect to stored `return_to` | No bearer auth. Mounted when `[creator_authority_acquisition].enabled = true`; stored `return_to` is revalidated before redirect. | `Location` contains only callback `state` and one-time `code`; no authorization URL, frontend session token, or creator authority secret. | `400 invalid_request`, `404 creator_connect_flow_unavailable`, `410 creator_connect_flow_expired`, `503 creator_authority_unavailable`, `404` when route gated off |
Expand Down Expand Up @@ -98,6 +99,7 @@ Stable error codes and statuses mirror `locks-server/src/api/errors.rs` tests:
| `frontend_session_expired` | 401 | Frontend session token existed but expired. |
| `frontend_session_state_mismatch` | 400 | One-time code exchange state did not match. |
| `creator_authority_unavailable` | 503 | Creator-granted homeserver authority is unavailable or could not be revalidated. |
| `content_lock_path_conflict` | 409 | The creator-scoped guarded path already has an in-flight or published Content Lock owner. |
| `task_state_conflict` | 409 | Submission or completion conflicts with existing task state. |
| `unsupported_verifier_type` | 422 | Proof references a verifier unavailable in the current runtime. |
| `paykit_not_configured` | 422 | A `paykit-payment` proof was submitted to a Lock Server without a `[paykit]` runtime section. |
Expand Down Expand Up @@ -348,11 +350,12 @@ Every referenced guarded resource must currently exist for the same creator/path
{
"recipient_pubky": "pubky<recipient>",
"amount": "50000",
"asset": "BTC"
"asset": "BTC",
"payment_in": 24
}
```

`recipient_pubky` must be a valid Pubky public key string equal to the content-lock creator, `amount` must be a positive base-unit integer encoded as a string, and `asset` must be a non-empty string. The lock params do not include Paykit server URLs, account IDs, memos, expiry, payment references, or reader identity. A v1 content lock that uses `paykit-payment` must contain exactly that one criterion, and its `all` or `any` lock logic must reference that criterion exactly once. Mixed criteria, multiple payment criteria, recipient/creator mismatch, and duplicate or mismatched logic references return `400 invalid_request`.
`recipient_pubky` must be a valid Pubky public key string equal to the content-lock creator, `amount` must be a positive base-unit integer encoded as a string, `asset` must be a non-empty string, and `payment_in` must be a positive whole-hour JSON `u64`. The lock params do not include Paykit server URLs, account IDs, memos, expiry, payment references, or reader identity. A v1 content lock that uses `paykit-payment` must contain exactly that one criterion, and its `all` or `any` lock logic must reference that criterion exactly once. Mixed criteria, multiple payment criteria, recipient/creator mismatch, and duplicate or mismatched logic references return `400 invalid_request`.

#### Request

Expand Down
Loading