Skip to content
Merged
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
34 changes: 34 additions & 0 deletions rest/nodejs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,40 @@ endpoint at:
http://localhost:3000/.well-known/ucp
```

## Request Signatures (RFC 9421)

The server verifies UCP request signatures as defined in the specification's
[`signatures.md`](https://github.com/Universal-Commerce-Protocol/ucp/blob/main/docs/specification/signatures.md):
[RFC 9421](https://www.rfc-editor.org/rfc/rfc9421.html) HTTP Message Signatures
with an [RFC 9530](https://www.rfc-editor.org/rfc/rfc9530.html) `Content-Digest`
over the raw body. The signer's public key is discovered from the profile URL in
the `UCP-Agent` header (its `keys[]`). `ES256` (fixed-width raw `r||s`, not
ASN.1/DER) is the baseline; `Ed25519` is also supported. The behaviour mirrors
the Python reference server (`rest/python/server`).

Behaviour is controlled by two environment variables:

| Variable | Default | Effect |
| ----------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `REQUIRE_SIGNATURES` | `false` | Reject requests whose signature is missing or invalid. When `false`, a present signature is still verified and the result logged, but unsigned or invalid requests are allowed — so existing clients keep working. |
| `ALLOW_INSECURE_PROFILE_URLS` | `false` | Permit `http` and loopback/private profile URLs when resolving keys. For localhost demos and CI only; it disables SSRF protections and must never be enabled in production. |

When verification fails under enforcement, the server returns the spec's error
code: `401 signature_missing` / `signature_invalid` / `key_not_found`,
`400 digest_mismatch` / `algorithm_unsupported` / `invalid_profile_url`,
`424 profile_unreachable`, or `422 profile_malformed`.

To reject anything unsigned, start the server with enforcement on:

```bash
REQUIRE_SIGNATURES=true npm run dev
```

Each verified request logs
`RFC 9421 signature verified (keyid=..., profile=...)`. The discovery profile
at `/.well-known/ucp` stays unverified: it is the public document a platform
must read before it can sign anything.

## Running Conformance Tests

To verify that this server implementation complies with the UCP specifications,
Expand Down
14 changes: 14 additions & 0 deletions rest/nodejs/src/api/checkout.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
// Copyright 2026 UCP Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { createHash } from "crypto";
import { type Context } from "hono";
import { v4 as uuidv4 } from "uuid";
Expand Down
14 changes: 14 additions & 0 deletions rest/nodejs/src/api/discovery.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
// Copyright 2026 UCP Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { type Context } from "hono";
import { UCP_VERSION } from "../utils/config";

Expand Down
14 changes: 14 additions & 0 deletions rest/nodejs/src/api/order.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
// Copyright 2026 UCP Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { getOrder, logRequest, saveOrder } from "../data";
import { type Order } from "../models";
import { type IdParamContext } from "../utils/validation";
Expand Down
14 changes: 14 additions & 0 deletions rest/nodejs/src/api/testing.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
// Copyright 2026 UCP Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { type IdParamContext } from "../utils/validation";
import { CheckoutService } from "./checkout";

Expand Down
14 changes: 14 additions & 0 deletions rest/nodejs/src/data/db.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
// Copyright 2026 UCP Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Database from "better-sqlite3";

let productsDb: Database.Database | null = null;
Expand Down
14 changes: 14 additions & 0 deletions rest/nodejs/src/data/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
// Copyright 2026 UCP Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
* @fileoverview Exports the data access layer for the UCP SDK Node.js server.
* This module provides functions to interact with the products, inventory, and
Expand Down
14 changes: 14 additions & 0 deletions rest/nodejs/src/data/inventory.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
// Copyright 2026 UCP Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { getTransactionsDb } from "./db";

/**
Expand Down
14 changes: 14 additions & 0 deletions rest/nodejs/src/data/products.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
// Copyright 2026 UCP Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { getProductsDb } from "./db";

/**
Expand Down
14 changes: 14 additions & 0 deletions rest/nodejs/src/data/transactions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
// Copyright 2026 UCP Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { type ExtendedCheckoutResponse, type Order } from "../models";

import { getTransactionsDb } from "./db";
Expand Down
28 changes: 28 additions & 0 deletions rest/nodejs/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
// Copyright 2026 UCP Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { serve } from "@hono/node-server";
import { zValidator } from "@hono/zod-validator";
import { type Context, Hono } from "hono";
Expand All @@ -15,6 +29,7 @@ import {
CheckoutCompleteRequestSchema,
OrderSchema,
} from "./models";
import { verifySignature } from "./utils/signature";
import { IdParamSchema, prettyValidation } from "./utils/validation";

const app = new Hono();
Expand Down Expand Up @@ -86,45 +101,57 @@ app.use(async (c: Context, next: () => Promise<void>) => {
});

/* Discovery endpoints */
// The discovery profile is served unverified: it is the public document a
// platform must be able to read before it can sign anything.
app.get("/.well-known/ucp", discoveryService.getMerchantProfile);

/* Checkout Capability endpoints */
// Every business endpoint below verifies RFC 9421 request signatures via
// verifySignature (enforced when REQUIRE_SIGNATURES=true, verify-and-log
// otherwise), mirroring the Python reference server.
app.post(
"/checkout-sessions",
verifySignature,
zValidator("json", ExtendedCheckoutCreateRequestSchema, prettyValidation),
checkoutService.createCheckout
);
app.get(
"/checkout-sessions/:id",
verifySignature,
zValidator("param", IdParamSchema, prettyValidation),
checkoutService.getCheckout
);
app.put(
"/checkout-sessions/:id",
verifySignature,
zValidator("param", IdParamSchema, prettyValidation),
zValidator("json", ExtendedCheckoutUpdateRequestSchema, prettyValidation),
checkoutService.updateCheckout
);
app.post(
"/checkout-sessions/:id/complete",
verifySignature,
zValidator("param", IdParamSchema, prettyValidation),
zValidator("json", CheckoutCompleteRequestSchema, prettyValidation),
checkoutService.completeCheckout
);
app.post(
"/checkout-sessions/:id/cancel",
verifySignature,
zValidator("param", IdParamSchema, prettyValidation),
checkoutService.cancelCheckout
);

/* Order Capability endpoints */
app.get(
"/orders/:id",
verifySignature,
zValidator("param", IdParamSchema, prettyValidation),
orderService.getOrder
);
app.put(
"/orders/:id",
verifySignature,
zValidator("param", IdParamSchema, prettyValidation),
zValidator("json", OrderSchema, prettyValidation),
orderService.updateOrder
Expand All @@ -133,6 +160,7 @@ app.put(
/* Testing endpoints */
app.post(
"/testing/simulate-shipping/:id",
verifySignature,
zValidator("param", IdParamSchema, prettyValidation),
testingService.shipOrder
);
Expand Down
14 changes: 14 additions & 0 deletions rest/nodejs/src/models/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
// Copyright 2026 UCP Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

export * from "@ucp-js/sdk";
24 changes: 24 additions & 0 deletions rest/nodejs/src/utils/config.ts
Original file line number Diff line number Diff line change
@@ -1 +1,25 @@
// Copyright 2026 UCP Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

export const UCP_VERSION = "2026-04-08";

// RFC 9421 request-signature behaviour, sourced from the environment like
// SIMULATION_SECRET in api/testing.ts. Both default to false: signatures are
// verified when present but unsigned or invalid requests are only logged, and
// profile URLs must be HTTPS on non-private hosts. Mutable so tests can toggle
// enforcement, mirroring the Python server's config.FLAGS.
export const signatureConfig = {
requireSignatures: process.env.REQUIRE_SIGNATURES === "true",
allowInsecureProfileUrls: process.env.ALLOW_INSECURE_PROFILE_URLS === "true",
};
Loading
Loading