@polymorfa/nextjs 0.1.0-dev.20260922094209
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/LICENSE +21 -0
- package/README.md +224 -0
- package/dist/development-store.d.ts +35 -0
- package/dist/development-store.d.ts.map +1 -0
- package/dist/development-store.js +186 -0
- package/dist/express.d.ts +21 -0
- package/dist/express.d.ts.map +1 -0
- package/dist/express.js +92 -0
- package/dist/handler.d.ts +229 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +434 -0
- package/dist/hono.d.ts +15 -0
- package/dist/hono.d.ts.map +1 -0
- package/dist/hono.js +11 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/media-route.d.ts +76 -0
- package/dist/media-route.d.ts.map +1 -0
- package/dist/media-route.js +151 -0
- package/dist/messaging-token-mint.d.ts +40 -0
- package/dist/messaging-token-mint.d.ts.map +1 -0
- package/dist/messaging-token-mint.js +38 -0
- package/dist/template-builder-route.d.ts +123 -0
- package/dist/template-builder-route.d.ts.map +1 -0
- package/dist/template-builder-route.js +187 -0
- package/dist/token-route.d.ts +19 -0
- package/dist/token-route.d.ts.map +1 -0
- package/dist/token-route.js +60 -0
- package/dist/webhook.d.ts +10 -0
- package/dist/webhook.d.ts.map +1 -0
- package/dist/webhook.js +16 -0
- package/package.json +54 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/** Creates a Next.js App Router-compatible, application-mediated template route. */
|
|
2
|
+
export function createTemplateBuilderRoute(options) {
|
|
3
|
+
validateOptions(options);
|
|
4
|
+
return async (request) => {
|
|
5
|
+
if (request.method !== "POST") {
|
|
6
|
+
return jsonError(405, "method_not_allowed", "Use POST for template builder actions.", { Allow: "POST" });
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
const subject = await options.authorize(request);
|
|
10
|
+
if (subject === null) {
|
|
11
|
+
return jsonError(401, "unauthorized", "Authentication is required.");
|
|
12
|
+
}
|
|
13
|
+
if (typeof subject.userId !== "string" || subject.userId.length === 0) {
|
|
14
|
+
throw new RouteInputError("invalid_subject", "userId is required.");
|
|
15
|
+
}
|
|
16
|
+
const action = await readAction(request);
|
|
17
|
+
const projectSlug = await options.resolveProjectSlug(subject, request);
|
|
18
|
+
requireString(projectSlug, "project_slug_required");
|
|
19
|
+
const signalOptions = { signal: request.signal };
|
|
20
|
+
switch (action.action) {
|
|
21
|
+
case "list": {
|
|
22
|
+
if (options.templates.list === undefined)
|
|
23
|
+
throw new RouteInputError("invalid_action", "Template listing is not enabled.");
|
|
24
|
+
const response = await options.templates.list(projectSlug, signalOptions);
|
|
25
|
+
const templates = responseData(response);
|
|
26
|
+
if (!Array.isArray(templates))
|
|
27
|
+
throw new TypeError("The server SDK returned an invalid template list.");
|
|
28
|
+
return json({ templates });
|
|
29
|
+
}
|
|
30
|
+
case "load": {
|
|
31
|
+
const response = await options.templates.retrieve(projectSlug, action.templateId, signalOptions);
|
|
32
|
+
return json({ template: responseData(response) });
|
|
33
|
+
}
|
|
34
|
+
case "save": {
|
|
35
|
+
const response = action.templateId === undefined
|
|
36
|
+
? await options.templates.create(projectSlug, action.draft, signalOptions)
|
|
37
|
+
: await options.templates.update(projectSlug, action.templateId, action.draft, signalOptions);
|
|
38
|
+
return json({ template: responseData(response) });
|
|
39
|
+
}
|
|
40
|
+
case "preview": {
|
|
41
|
+
const response = await options.templates.preview(projectSlug, action.templateId, {
|
|
42
|
+
...(action.values === undefined ? {} : { values: action.values }),
|
|
43
|
+
...(action.surface === undefined
|
|
44
|
+
? {}
|
|
45
|
+
: { surface: action.surface }),
|
|
46
|
+
}, signalOptions);
|
|
47
|
+
return json({ preview: responseData(response) });
|
|
48
|
+
}
|
|
49
|
+
case "submit": {
|
|
50
|
+
const session = await options.resolveSubmissionSession(subject, request);
|
|
51
|
+
requireString(session, "submission_session_required");
|
|
52
|
+
const response = await options.templates.submit(projectSlug, action.templateId, { session }, signalOptions);
|
|
53
|
+
return json({ template: responseData(response) });
|
|
54
|
+
}
|
|
55
|
+
case "delete":
|
|
56
|
+
await options.templates.delete(projectSlug, action.templateId, signalOptions);
|
|
57
|
+
return json({ deleted: true });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error instanceof RouteInputError) {
|
|
62
|
+
return jsonError(400, error.code, error.message);
|
|
63
|
+
}
|
|
64
|
+
return jsonError(500, "template_action_failed", "Unable to complete the template action.");
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async function readAction(request) {
|
|
69
|
+
let value;
|
|
70
|
+
try {
|
|
71
|
+
value = await request.json();
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new RouteInputError("invalid_json", "A JSON request body is required.");
|
|
75
|
+
}
|
|
76
|
+
if (!isRecord(value) || typeof value.action !== "string") {
|
|
77
|
+
throw new RouteInputError("invalid_action", "A template action is required.");
|
|
78
|
+
}
|
|
79
|
+
if (value.action === "list")
|
|
80
|
+
return { action: "list" };
|
|
81
|
+
if (value.action === "save") {
|
|
82
|
+
const draft = readDraft(value.draft);
|
|
83
|
+
const templateId = optionalString(value.templateId, "template_id_invalid");
|
|
84
|
+
return {
|
|
85
|
+
action: "save",
|
|
86
|
+
draft,
|
|
87
|
+
...(templateId === undefined ? {} : { templateId }),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (value.action !== "load" &&
|
|
91
|
+
value.action !== "preview" &&
|
|
92
|
+
value.action !== "submit" &&
|
|
93
|
+
value.action !== "delete") {
|
|
94
|
+
throw new RouteInputError("invalid_action", "Unknown template action.");
|
|
95
|
+
}
|
|
96
|
+
const templateId = requireString(value.templateId, "template_id_required");
|
|
97
|
+
if (value.action === "preview") {
|
|
98
|
+
return {
|
|
99
|
+
action: "preview",
|
|
100
|
+
templateId,
|
|
101
|
+
...(value.values === undefined
|
|
102
|
+
? {}
|
|
103
|
+
: { values: readValues(value.values) }),
|
|
104
|
+
...(value.surface === undefined
|
|
105
|
+
? {}
|
|
106
|
+
: { surface: readSurface(value.surface) }),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return { action: value.action, templateId };
|
|
110
|
+
}
|
|
111
|
+
function readDraft(value) {
|
|
112
|
+
if (!isRecord(value)) {
|
|
113
|
+
throw new RouteInputError("draft_required", "A template draft is required.");
|
|
114
|
+
}
|
|
115
|
+
const name = requireString(value.name, "template_name_required");
|
|
116
|
+
if (!isRecord(value.definition)) {
|
|
117
|
+
throw new RouteInputError("template_definition_required", "A template definition is required.");
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
name,
|
|
121
|
+
definition: value.definition,
|
|
122
|
+
...(value.sampleValues === undefined
|
|
123
|
+
? {}
|
|
124
|
+
: { sampleValues: readValues(value.sampleValues) }),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function readValues(value) {
|
|
128
|
+
if (!isRecord(value) ||
|
|
129
|
+
Object.values(value).some((item) => typeof item !== "string")) {
|
|
130
|
+
throw new RouteInputError("template_values_invalid", "Template values must be strings.");
|
|
131
|
+
}
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
function readSurface(value) {
|
|
135
|
+
if (value !== "cloud" && value !== "whatsmeow" && value !== "sandbox") {
|
|
136
|
+
throw new RouteInputError("template_surface_invalid", "Unknown template surface.");
|
|
137
|
+
}
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
function responseData(response) {
|
|
141
|
+
if (!response?.data?.success || response.data.data === undefined) {
|
|
142
|
+
throw new TypeError("The server SDK returned an invalid template response.");
|
|
143
|
+
}
|
|
144
|
+
return response.data.data;
|
|
145
|
+
}
|
|
146
|
+
function validateOptions(options) {
|
|
147
|
+
if (typeof options.authorize !== "function" ||
|
|
148
|
+
typeof options.resolveProjectSlug !== "function" ||
|
|
149
|
+
typeof options.resolveSubmissionSession !== "function" ||
|
|
150
|
+
typeof options.templates?.create !== "function" ||
|
|
151
|
+
typeof options.templates?.retrieve !== "function" ||
|
|
152
|
+
typeof options.templates?.update !== "function" ||
|
|
153
|
+
typeof options.templates?.delete !== "function" ||
|
|
154
|
+
typeof options.templates?.preview !== "function" ||
|
|
155
|
+
typeof options.templates?.submit !== "function") {
|
|
156
|
+
throw new TypeError("Template route options are incomplete.");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function requireString(value, code) {
|
|
160
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
161
|
+
throw new RouteInputError(code, "A non-empty string is required.");
|
|
162
|
+
}
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
function optionalString(value, code) {
|
|
166
|
+
return value === undefined ? undefined : requireString(value, code);
|
|
167
|
+
}
|
|
168
|
+
function isRecord(value) {
|
|
169
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
170
|
+
}
|
|
171
|
+
const privateHeaders = {
|
|
172
|
+
"Cache-Control": "no-store, private",
|
|
173
|
+
Vary: "Cookie, Authorization",
|
|
174
|
+
};
|
|
175
|
+
function json(value) {
|
|
176
|
+
return Response.json(value, { status: 200, headers: privateHeaders });
|
|
177
|
+
}
|
|
178
|
+
function jsonError(status, code, message, headers = {}) {
|
|
179
|
+
return Response.json({ error: { code, message } }, { status, headers: { ...privateHeaders, ...headers } });
|
|
180
|
+
}
|
|
181
|
+
class RouteInputError extends Error {
|
|
182
|
+
code;
|
|
183
|
+
constructor(code, message) {
|
|
184
|
+
super(message);
|
|
185
|
+
this.code = code;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ClientTokenSubject {
|
|
2
|
+
readonly userId: string;
|
|
3
|
+
readonly projectId?: string;
|
|
4
|
+
readonly organizationId?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ClientTokenClaims {
|
|
7
|
+
readonly value: string;
|
|
8
|
+
readonly audience: "browser";
|
|
9
|
+
readonly expiresAt: number;
|
|
10
|
+
readonly scopes?: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
export type ClientTokenMint = (subject: ClientTokenSubject, request: Request) => Promise<ClientTokenClaims>;
|
|
13
|
+
export interface ClientTokenRouteOptions {
|
|
14
|
+
readonly mint: ClientTokenMint;
|
|
15
|
+
readonly authorize: (request: Request) => Promise<ClientTokenSubject | null> | ClientTokenSubject | null;
|
|
16
|
+
}
|
|
17
|
+
/** Creates a Next.js App Router-compatible POST route without importing Next.js. */
|
|
18
|
+
export declare function createClientTokenRoute(options: ClientTokenRouteOptions): (request: Request) => Promise<Response>;
|
|
19
|
+
//# sourceMappingURL=token-route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-route.d.ts","sourceRoot":"","sources":["../src/token-route.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,MAAM,MAAM,eAAe,GAAG,CAC5B,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,OAAO,KACb,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAEhC,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,CAClB,OAAO,EAAE,OAAO,KACb,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,GAAG,kBAAkB,GAAG,IAAI,CAAC;CACrE;AAED,oFAAoF;AACpF,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,uBAAuB,GAC/B,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA2CzC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Creates a Next.js App Router-compatible POST route without importing Next.js. */
|
|
2
|
+
export function createClientTokenRoute(options) {
|
|
3
|
+
if (typeof options.authorize !== "function" ||
|
|
4
|
+
typeof options.mint !== "function") {
|
|
5
|
+
throw new TypeError("authorize and mint must be functions.");
|
|
6
|
+
}
|
|
7
|
+
return async (request) => {
|
|
8
|
+
if (request.method !== "POST") {
|
|
9
|
+
return jsonError(405, "method_not_allowed", "Use POST to mint a client token.", {
|
|
10
|
+
Allow: "POST",
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const subject = await options.authorize(request);
|
|
15
|
+
if (subject === null) {
|
|
16
|
+
return jsonError(401, "unauthorized", "Authentication is required.");
|
|
17
|
+
}
|
|
18
|
+
validateSubject(subject);
|
|
19
|
+
const token = await options.mint(subject, request);
|
|
20
|
+
validateToken(token);
|
|
21
|
+
return Response.json(token, {
|
|
22
|
+
status: 200,
|
|
23
|
+
headers: privateHeaders,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error instanceof RouteInputError) {
|
|
28
|
+
return jsonError(400, error.code, error.message);
|
|
29
|
+
}
|
|
30
|
+
return jsonError(500, "token_mint_failed", "Unable to mint a client token.");
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const privateHeaders = {
|
|
35
|
+
"Cache-Control": "no-store, private",
|
|
36
|
+
Vary: "Cookie, Authorization",
|
|
37
|
+
};
|
|
38
|
+
function jsonError(status, code, message, headers = {}) {
|
|
39
|
+
return Response.json({ error: { code, message } }, { status, headers: { ...privateHeaders, ...headers } });
|
|
40
|
+
}
|
|
41
|
+
function validateSubject(subject) {
|
|
42
|
+
if (typeof subject.userId !== "string" || subject.userId.length === 0) {
|
|
43
|
+
throw new RouteInputError("invalid_subject", "userId is required.");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function validateToken(token) {
|
|
47
|
+
if (!token.value.startsWith("pmfa_ct_") || token.audience !== "browser") {
|
|
48
|
+
throw new RouteInputError("invalid_client_token", "Mint returned an invalid client token.");
|
|
49
|
+
}
|
|
50
|
+
if (!Number.isFinite(token.expiresAt) || token.expiresAt <= Date.now()) {
|
|
51
|
+
throw new RouteInputError("invalid_client_token", "Mint returned an expired client token.");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
class RouteInputError extends Error {
|
|
55
|
+
code;
|
|
56
|
+
constructor(code, message) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.code = code;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type WebhookConstructor<TEvent> = (rawBody: ArrayBuffer, signature: string, secret: string) => Promise<TEvent>;
|
|
2
|
+
export interface WebhookRequestOptions<TEvent> {
|
|
3
|
+
/** Pass `constructWebhookEvent` from `@polymorfa/sdk`. */
|
|
4
|
+
readonly constructEvent: WebhookConstructor<TEvent>;
|
|
5
|
+
readonly secret: string;
|
|
6
|
+
readonly signatureHeader?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Reads the raw body exactly once, then delegates signature verification and parsing to the server SDK. */
|
|
9
|
+
export declare function readVerifiedWebhook<TEvent>(request: Request, options: WebhookRequestOptions<TEvent>): Promise<TEvent>;
|
|
10
|
+
//# sourceMappingURL=webhook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../src/webhook.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,CAAC,MAAM,IAAI,CACvC,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,KACX,OAAO,CAAC,MAAM,CAAC,CAAC;AAErB,MAAM,WAAW,qBAAqB,CAAC,MAAM;IAC3C,0DAA0D;IAC1D,QAAQ,CAAC,cAAc,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC;IACpD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CACnC;AAED,4GAA4G;AAC5G,wBAAsB,mBAAmB,CAAC,MAAM,EAC9C,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,qBAAqB,CAAC,MAAM,CAAC,GACrC,OAAO,CAAC,MAAM,CAAC,CAcjB"}
|
package/dist/webhook.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Reads the raw body exactly once, then delegates signature verification and parsing to the server SDK. */
|
|
2
|
+
export async function readVerifiedWebhook(request, options) {
|
|
3
|
+
if (request.bodyUsed) {
|
|
4
|
+
throw new TypeError("Webhook request body has already been consumed.");
|
|
5
|
+
}
|
|
6
|
+
if (options.secret.length === 0) {
|
|
7
|
+
throw new TypeError("Webhook secret must not be empty.");
|
|
8
|
+
}
|
|
9
|
+
const headerName = options.signatureHeader ?? "x-webhook-signature";
|
|
10
|
+
const signature = request.headers.get(headerName);
|
|
11
|
+
if (signature === null || signature.length === 0) {
|
|
12
|
+
throw new TypeError(`Missing webhook signature header: ${headerName}.`);
|
|
13
|
+
}
|
|
14
|
+
const rawBody = await request.arrayBuffer();
|
|
15
|
+
return options.constructEvent(rawBody, signature, options.secret);
|
|
16
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@polymorfa/nextjs",
|
|
3
|
+
"version": "0.1.0-dev.20260922094209",
|
|
4
|
+
"description": "Server-only helpers for Polymorfa: one catch-all handler for Next.js, Express and Hono, client tokens, webhooks and media",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/polymorfa/sdks.git",
|
|
10
|
+
"directory": "packages/nextjs"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/polymorfa/sdks/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/polymorfa/sdks#readme",
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./express": {
|
|
25
|
+
"types": "./dist/express.d.ts",
|
|
26
|
+
"import": "./dist/express.js"
|
|
27
|
+
},
|
|
28
|
+
"./hono": {
|
|
29
|
+
"types": "./dist/hono.d.ts",
|
|
30
|
+
"import": "./dist/hono.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc -p tsconfig.build.json",
|
|
40
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@polymorfa/sdk": "0.1.0-dev.20260922094209"
|
|
44
|
+
},
|
|
45
|
+
"peerDependenciesMeta": {
|
|
46
|
+
"@polymorfa/sdk": {
|
|
47
|
+
"optional": true
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public",
|
|
52
|
+
"provenance": true
|
|
53
|
+
}
|
|
54
|
+
}
|