@reactionary/ucp 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +154 -0
- package/index.js +1 -0
- package/lib/reactionary-ucp-capabilities.js +19 -0
- package/lib/reactionary-ucp-common.js +0 -0
- package/lib/reactionary-ucp-http.js +79 -0
- package/lib/reactionary-ucp-profile.js +72 -0
- package/lib/reactionary-ucp-rest.js +715 -0
- package/lib/reactionary-ucp-server.js +102 -0
- package/lib/reactionary-ucp-session-store.js +63 -0
- package/lib/ucp-shopping.openapi.js +0 -0
- package/package.json +19 -0
- package/src/index.d.ts +1 -0
- package/src/lib/reactionary-ucp-capabilities.d.ts +4 -0
- package/src/lib/reactionary-ucp-common.d.ts +71 -0
- package/src/lib/reactionary-ucp-http.d.ts +12 -0
- package/src/lib/reactionary-ucp-profile.d.ts +5 -0
- package/src/lib/reactionary-ucp-rest.d.ts +8 -0
- package/src/lib/reactionary-ucp-server.d.ts +14 -0
- package/src/lib/reactionary-ucp-session-store.d.ts +23 -0
- package/src/lib/ucp-shopping.openapi.d.ts +2632 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createInitialRequestContext,
|
|
3
|
+
MemoryCache
|
|
4
|
+
} from "@reactionary/core";
|
|
5
|
+
import {
|
|
6
|
+
} from "./reactionary-ucp-common.js";
|
|
7
|
+
import { getOrCreateSessionId, jsonResponse, sendWebResponse, toWebRequest, UCP_SESSION_ID_HEADER } from "./reactionary-ucp-http.js";
|
|
8
|
+
import { createUCPProfile, getRequestRoute } from "./reactionary-ucp-profile.js";
|
|
9
|
+
import { handleRestRequest, UCPHttpError } from "./reactionary-ucp-rest.js";
|
|
10
|
+
import { ReactionaryUCPSessionStore } from "./reactionary-ucp-session-store.js";
|
|
11
|
+
class ReactionaryUCPServer {
|
|
12
|
+
constructor(clientFactory, options = {}) {
|
|
13
|
+
this.clientFactory = clientFactory;
|
|
14
|
+
this.options = options;
|
|
15
|
+
this.sessionStore = new ReactionaryUCPSessionStore(
|
|
16
|
+
this.options.sessionCache ?? new MemoryCache(),
|
|
17
|
+
this.options.sessionTtlSeconds ?? 60 * 60 * 24
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
clientFactory;
|
|
21
|
+
options;
|
|
22
|
+
sessionStore;
|
|
23
|
+
async fetch(request) {
|
|
24
|
+
const sessionId = getOrCreateSessionId(request);
|
|
25
|
+
const requestContext = await this.createRequestContext(sessionId);
|
|
26
|
+
const client = this.clientFactory(requestContext);
|
|
27
|
+
const response = await this.handleRequest(request, client, sessionId);
|
|
28
|
+
await this.sessionStore.put(sessionId, requestContext.session);
|
|
29
|
+
response.headers.set(UCP_SESSION_ID_HEADER, sessionId);
|
|
30
|
+
const requestId = request.headers.get("Request-Id");
|
|
31
|
+
if (requestId) {
|
|
32
|
+
response.headers.set("Request-Id", requestId);
|
|
33
|
+
}
|
|
34
|
+
return response;
|
|
35
|
+
}
|
|
36
|
+
getHandler() {
|
|
37
|
+
return {
|
|
38
|
+
fetch: (request) => this.fetch(request),
|
|
39
|
+
close: () => this.close()
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
toNodeHandler() {
|
|
43
|
+
return async (request, response) => {
|
|
44
|
+
const webResponse = await this.fetch(await toWebRequest(request));
|
|
45
|
+
await sendWebResponse(response, webResponse);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
close() {
|
|
49
|
+
return Promise.resolve();
|
|
50
|
+
}
|
|
51
|
+
async createRequestContext(sessionId) {
|
|
52
|
+
const restoredSession = await this.sessionStore.get(sessionId);
|
|
53
|
+
const requestContext = createInitialRequestContext();
|
|
54
|
+
if (restoredSession) {
|
|
55
|
+
requestContext.session = restoredSession;
|
|
56
|
+
}
|
|
57
|
+
return requestContext;
|
|
58
|
+
}
|
|
59
|
+
async handleRequest(request, client, sessionId) {
|
|
60
|
+
const route = getRequestRoute(request, this.options.profile);
|
|
61
|
+
if (request.method === "OPTIONS") {
|
|
62
|
+
return new Response(null, {
|
|
63
|
+
status: 204,
|
|
64
|
+
headers: {
|
|
65
|
+
allow: "GET, HEAD, OPTIONS, POST, PUT"
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (route.path === "/.well-known/ucp" && request.method === "GET") {
|
|
70
|
+
return jsonResponse(createUCPProfile(client, this.options.profile));
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const restResponse = await handleRestRequest(
|
|
74
|
+
request,
|
|
75
|
+
client,
|
|
76
|
+
route.path,
|
|
77
|
+
sessionId,
|
|
78
|
+
this.sessionStore
|
|
79
|
+
);
|
|
80
|
+
if (restResponse) {
|
|
81
|
+
return restResponse;
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (error instanceof UCPHttpError) {
|
|
85
|
+
return jsonResponse(error.body, { status: error.status });
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
return jsonResponse({
|
|
90
|
+
error: {
|
|
91
|
+
code: "NOT_FOUND",
|
|
92
|
+
message: `No UCP route matched ${request.method} ${route.path}.`
|
|
93
|
+
}
|
|
94
|
+
}, {
|
|
95
|
+
status: 404,
|
|
96
|
+
omitBody: request.method === "HEAD"
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export {
|
|
101
|
+
ReactionaryUCPServer
|
|
102
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { SessionSchema } from "@reactionary/core";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
const SESSION_CACHE_KEY_PREFIX = "reactionary:ucp:session";
|
|
4
|
+
const IDEMPOTENCY_CACHE_KEY_PREFIX = "reactionary:ucp:idempotency";
|
|
5
|
+
const UCPActionResponseSchema = z.looseObject({});
|
|
6
|
+
const UCPIdempotencyRecordSchema = z.looseObject({
|
|
7
|
+
action: z.string(),
|
|
8
|
+
fingerprint: z.string().optional(),
|
|
9
|
+
status: z.number().optional(),
|
|
10
|
+
response: UCPActionResponseSchema
|
|
11
|
+
});
|
|
12
|
+
class ReactionaryUCPSessionStore {
|
|
13
|
+
constructor(cache, ttlSeconds) {
|
|
14
|
+
this.cache = cache;
|
|
15
|
+
this.ttlSeconds = ttlSeconds;
|
|
16
|
+
}
|
|
17
|
+
cache;
|
|
18
|
+
ttlSeconds;
|
|
19
|
+
async get(sessionId) {
|
|
20
|
+
return await this.cache.get(
|
|
21
|
+
this.getCacheKey(sessionId),
|
|
22
|
+
SessionSchema
|
|
23
|
+
) ?? void 0;
|
|
24
|
+
}
|
|
25
|
+
async put(sessionId, session) {
|
|
26
|
+
await this.cache.invalidate([this.getDependencyId(sessionId)]);
|
|
27
|
+
await this.cache.put(this.getCacheKey(sessionId), session, {
|
|
28
|
+
ttlSeconds: this.ttlSeconds,
|
|
29
|
+
dependencyIds: [this.getDependencyId(sessionId)]
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async getIdempotencyRecord(sessionId, idempotencyKey) {
|
|
33
|
+
return await this.cache.get(
|
|
34
|
+
this.getIdempotencyCacheKey(sessionId, idempotencyKey),
|
|
35
|
+
UCPIdempotencyRecordSchema
|
|
36
|
+
) ?? void 0;
|
|
37
|
+
}
|
|
38
|
+
async putIdempotencyRecord(sessionId, idempotencyKey, record) {
|
|
39
|
+
await this.cache.put(
|
|
40
|
+
this.getIdempotencyCacheKey(sessionId, idempotencyKey),
|
|
41
|
+
record,
|
|
42
|
+
{
|
|
43
|
+
ttlSeconds: this.ttlSeconds,
|
|
44
|
+
dependencyIds: [this.getIdempotencyDependencyId(sessionId)]
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
getCacheKey(sessionId) {
|
|
49
|
+
return `${SESSION_CACHE_KEY_PREFIX}:${sessionId}`;
|
|
50
|
+
}
|
|
51
|
+
getDependencyId(sessionId) {
|
|
52
|
+
return `${SESSION_CACHE_KEY_PREFIX}:${sessionId}`;
|
|
53
|
+
}
|
|
54
|
+
getIdempotencyCacheKey(sessionId, idempotencyKey) {
|
|
55
|
+
return `${IDEMPOTENCY_CACHE_KEY_PREFIX}:${sessionId}:${encodeURIComponent(idempotencyKey)}`;
|
|
56
|
+
}
|
|
57
|
+
getIdempotencyDependencyId(sessionId) {
|
|
58
|
+
return `${IDEMPOTENCY_CACHE_KEY_PREFIX}:${sessionId}`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
ReactionaryUCPSessionStore
|
|
63
|
+
};
|
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reactionary/ucp",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./index.js",
|
|
6
|
+
"types": "./src/index.d.ts",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Solteq/reactionary.git"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@reactionary/core": "0.0.1",
|
|
13
|
+
"zod": "4.1.9"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"vitest": "^4.0.9"
|
|
17
|
+
},
|
|
18
|
+
"sideEffects": false
|
|
19
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './lib/reactionary-ucp-server.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { BaseCapability } from '@reactionary/core';
|
|
2
|
+
import type { ReactionaryUCPClient } from './reactionary-ucp-common.js';
|
|
3
|
+
export declare function hasCapability(client: ReactionaryUCPClient, name: string): boolean;
|
|
4
|
+
export declare function getCapability(client: ReactionaryUCPClient, name: string): BaseCapability | undefined;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Cache, Cart, Checkout, Client, FacetValueIdentifier, Order, Product, ProductSearchResult, RequestContext, Result } from '@reactionary/core';
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
|
+
import type { components } from './ucp-shopping.openapi.js';
|
|
4
|
+
type UCPService = components['schemas']['base'];
|
|
5
|
+
type UCPCapability = components['schemas']['$defs-base'];
|
|
6
|
+
type UCPPaymentHandler = components['schemas']['payment_handler_$defs-base'];
|
|
7
|
+
export interface ReactionaryUCPProfile {
|
|
8
|
+
ucp: {
|
|
9
|
+
version: string;
|
|
10
|
+
services: Record<string, UCPService[]>;
|
|
11
|
+
capabilities: Record<string, UCPCapability[]>;
|
|
12
|
+
payment_handlers?: Record<string, UCPPaymentHandler[]>;
|
|
13
|
+
};
|
|
14
|
+
keys: Array<Record<string, unknown>>;
|
|
15
|
+
merchant: {
|
|
16
|
+
name: string;
|
|
17
|
+
url: string;
|
|
18
|
+
contact: {
|
|
19
|
+
email: string;
|
|
20
|
+
phone_number?: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
type UCPMethod<TMethod extends (payload: never) => Promise<unknown>, TValue> = (payload: Parameters<TMethod>[0]) => Promise<Result<TValue, unknown>>;
|
|
25
|
+
export type ReactionaryUCPClient = object & {
|
|
26
|
+
productSearch?: {
|
|
27
|
+
createCategoryNavigationFilter?: UCPMethod<Client['productSearch']['createCategoryNavigationFilter'], FacetValueIdentifier>;
|
|
28
|
+
queryByTerm: UCPMethod<Client['productSearch']['queryByTerm'], ProductSearchResult>;
|
|
29
|
+
};
|
|
30
|
+
product?: {
|
|
31
|
+
getById: UCPMethod<Client['product']['getById'], Product>;
|
|
32
|
+
getBySKU: UCPMethod<Client['product']['getBySKU'], Product>;
|
|
33
|
+
};
|
|
34
|
+
cart?: {
|
|
35
|
+
add: UCPMethod<Client['cart']['add'], Cart>;
|
|
36
|
+
changeQuantity: UCPMethod<Client['cart']['changeQuantity'], Cart>;
|
|
37
|
+
createCart: UCPMethod<Client['cart']['createCart'], Cart>;
|
|
38
|
+
deleteCart: UCPMethod<Client['cart']['deleteCart'], void>;
|
|
39
|
+
getById: UCPMethod<Client['cart']['getById'], Cart>;
|
|
40
|
+
remove: UCPMethod<Client['cart']['remove'], Cart>;
|
|
41
|
+
};
|
|
42
|
+
checkout?: {
|
|
43
|
+
addPaymentInstruction: UCPMethod<Client['checkout']['addPaymentInstruction'], Checkout>;
|
|
44
|
+
finalizeCheckout: UCPMethod<Client['checkout']['finalizeCheckout'], Checkout>;
|
|
45
|
+
getById: UCPMethod<Client['checkout']['getById'], Checkout>;
|
|
46
|
+
initiateCheckoutForCart: UCPMethod<Client['checkout']['initiateCheckoutForCart'], Checkout>;
|
|
47
|
+
};
|
|
48
|
+
order?: {
|
|
49
|
+
getById: UCPMethod<Client['order']['getById'], Order>;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
export type ReactionaryUCPClientFactory<TClient extends ReactionaryUCPClient = ReactionaryUCPClient> = (requestContext: RequestContext) => TClient;
|
|
53
|
+
export interface ReactionaryUCPServerOptions {
|
|
54
|
+
sessionCache?: Cache;
|
|
55
|
+
sessionTtlSeconds?: number;
|
|
56
|
+
profile?: ReactionaryUCPProfileOptions;
|
|
57
|
+
}
|
|
58
|
+
export interface ReactionaryUCPProfileOptions {
|
|
59
|
+
endpoint: string;
|
|
60
|
+
merchant: ReactionaryUCPProfile['merchant'];
|
|
61
|
+
keys: ReactionaryUCPProfile['keys'];
|
|
62
|
+
services?: ReactionaryUCPProfile['ucp']['services'];
|
|
63
|
+
capabilities?: ReactionaryUCPProfile['ucp']['capabilities'];
|
|
64
|
+
paymentHandlers?: ReactionaryUCPProfile['ucp']['payment_handlers'];
|
|
65
|
+
}
|
|
66
|
+
export interface ReactionaryUCPHttpHandler {
|
|
67
|
+
fetch(request: Request): Promise<Response>;
|
|
68
|
+
close(): Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
export type ReactionaryUCPNodeRequestHandler = (request: IncomingMessage, response: ServerResponse) => Promise<void>;
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
export declare const UCP_SESSION_ID_HEADER = "ucp-session-id";
|
|
3
|
+
type ProtocolHeaders = Headers | Record<string, string>;
|
|
4
|
+
export declare function getOrCreateSessionId(request: Request): string;
|
|
5
|
+
export declare function jsonResponse(body: unknown, options?: {
|
|
6
|
+
status?: number;
|
|
7
|
+
headers?: ProtocolHeaders;
|
|
8
|
+
omitBody?: boolean;
|
|
9
|
+
}): Response;
|
|
10
|
+
export declare function toWebRequest(request: IncomingMessage): Promise<Request>;
|
|
11
|
+
export declare function sendWebResponse(response: ServerResponse, webResponse: Response): Promise<void>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ReactionaryUCPClient, ReactionaryUCPProfile, ReactionaryUCPProfileOptions } from './reactionary-ucp-common.js';
|
|
2
|
+
export declare function getRequestRoute(request: Request, profile: ReactionaryUCPProfileOptions | undefined): {
|
|
3
|
+
path: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function createUCPProfile(client: ReactionaryUCPClient, profile: ReactionaryUCPProfileOptions | undefined): ReactionaryUCPProfile;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ReactionaryUCPClient } from './reactionary-ucp-common.js';
|
|
2
|
+
import type { ReactionaryUCPSessionStore } from './reactionary-ucp-session-store.js';
|
|
3
|
+
export declare function handleRestRequest(request: Request, client: ReactionaryUCPClient, path: string, sessionId: string, sessionStore: ReactionaryUCPSessionStore): Promise<Response | undefined>;
|
|
4
|
+
export declare class UCPHttpError extends Error {
|
|
5
|
+
readonly status: number;
|
|
6
|
+
readonly body: unknown;
|
|
7
|
+
constructor(status: number, body: unknown);
|
|
8
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ReactionaryUCPClient, type ReactionaryUCPClientFactory, type ReactionaryUCPHttpHandler, type ReactionaryUCPNodeRequestHandler, type ReactionaryUCPProfile, type ReactionaryUCPProfileOptions, type ReactionaryUCPServerOptions } from './reactionary-ucp-common.js';
|
|
2
|
+
export type { ReactionaryUCPClient, ReactionaryUCPClientFactory, ReactionaryUCPHttpHandler, ReactionaryUCPNodeRequestHandler, ReactionaryUCPProfile, ReactionaryUCPProfileOptions, ReactionaryUCPServerOptions, };
|
|
3
|
+
export declare class ReactionaryUCPServer<TClient extends ReactionaryUCPClient = ReactionaryUCPClient> {
|
|
4
|
+
private readonly clientFactory;
|
|
5
|
+
private readonly options;
|
|
6
|
+
private readonly sessionStore;
|
|
7
|
+
constructor(clientFactory: ReactionaryUCPClientFactory<TClient>, options?: ReactionaryUCPServerOptions);
|
|
8
|
+
fetch(request: Request): Promise<Response>;
|
|
9
|
+
getHandler(): ReactionaryUCPHttpHandler;
|
|
10
|
+
toNodeHandler(): ReactionaryUCPNodeRequestHandler;
|
|
11
|
+
close(): Promise<void>;
|
|
12
|
+
private createRequestContext;
|
|
13
|
+
private handleRequest;
|
|
14
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type Cache, type Session } from '@reactionary/core';
|
|
2
|
+
import * as z from 'zod';
|
|
3
|
+
declare const UCPIdempotencyRecordSchema: z.ZodObject<{
|
|
4
|
+
action: z.ZodString;
|
|
5
|
+
fingerprint: z.ZodOptional<z.ZodString>;
|
|
6
|
+
status: z.ZodOptional<z.ZodNumber>;
|
|
7
|
+
response: z.ZodObject<{}, z.core.$loose>;
|
|
8
|
+
}, z.core.$loose>;
|
|
9
|
+
export type UCPIdempotencyRecord = z.infer<typeof UCPIdempotencyRecordSchema>;
|
|
10
|
+
export declare class ReactionaryUCPSessionStore {
|
|
11
|
+
private readonly cache;
|
|
12
|
+
private readonly ttlSeconds;
|
|
13
|
+
constructor(cache: Cache, ttlSeconds: number);
|
|
14
|
+
get(sessionId: string): Promise<Session | undefined>;
|
|
15
|
+
put(sessionId: string, session: Session): Promise<void>;
|
|
16
|
+
getIdempotencyRecord(sessionId: string, idempotencyKey: string): Promise<UCPIdempotencyRecord | undefined>;
|
|
17
|
+
putIdempotencyRecord(sessionId: string, idempotencyKey: string, record: UCPIdempotencyRecord): Promise<void>;
|
|
18
|
+
private getCacheKey;
|
|
19
|
+
private getDependencyId;
|
|
20
|
+
private getIdempotencyCacheKey;
|
|
21
|
+
private getIdempotencyDependencyId;
|
|
22
|
+
}
|
|
23
|
+
export {};
|