@onabl/next 0.1.0
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 +18 -0
- package/dist/index.d.mts +57 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +157 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +126 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# `@onabl/next`
|
|
2
|
+
|
|
3
|
+
Next.js integration for the onabl SDK. Owns the secret-key-holding server
|
|
4
|
+
layer so an integrator's client components never see it — see
|
|
5
|
+
`docs/SDK-DESIGN.md` §1 for the two-file, eleven-line target this package
|
|
6
|
+
exists to make possible.
|
|
7
|
+
|
|
8
|
+
**Status:** skeleton only (Phase 14 §14.5.4, not started — `@onabl/react`
|
|
9
|
+
has to land first).
|
|
10
|
+
|
|
11
|
+
## What belongs here
|
|
12
|
+
|
|
13
|
+
- `createOnablClient(config)` — re-exported from `@onabl/react`/`@onabl/js`
|
|
14
|
+
with Next-aware cookie handling
|
|
15
|
+
- `onabl.routes()` — the `{GET, POST, PATCH}` route-handler factory, mounted
|
|
16
|
+
at `app/api/onabl/[...onabl]/route.ts` in a consumer's app
|
|
17
|
+
- The RSC-safe boundary that makes it structurally impossible to pass server
|
|
18
|
+
config (the secret key) into a client component
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { OnablClient } from '@onabl/js';
|
|
3
|
+
|
|
4
|
+
interface OnablNextConfig {
|
|
5
|
+
handle: string;
|
|
6
|
+
secretKey: string;
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
apiVersion?: string;
|
|
9
|
+
quotePath?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface OnablRouteHandlers {
|
|
13
|
+
GET: (req: NextRequest) => Promise<Response>;
|
|
14
|
+
POST: (req: NextRequest) => Promise<Response>;
|
|
15
|
+
PATCH: (req: NextRequest) => Promise<Response>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Route-handler factory for the same-origin proxy every @onabl/react hook
|
|
19
|
+
* calls (see packages/react/src/context.tsx) — the reference implementation
|
|
20
|
+
* of onabl.routes() described in docs/SDK-DESIGN.md §14.5.4. Mount the
|
|
21
|
+
* returned {GET, POST, PATCH} at a single catch-all route matching
|
|
22
|
+
* `basePath`, e.g. `app/api/onabl/[...onabl]/route.ts`:
|
|
23
|
+
*
|
|
24
|
+
* export const {GET, POST, PATCH} = createOnablRouteHandlers({
|
|
25
|
+
* handle: "acme",
|
|
26
|
+
* secretKey: process.env.ONABL_SECRET_KEY!,
|
|
27
|
+
* basePath: "/api/onabl",
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* Holds config.secretKey server-side only — it never reaches the browser.
|
|
31
|
+
* Customer access tokens (quote/invoice-scoped, not secrets in the same
|
|
32
|
+
* sense) are read from/written to an httpOnly cookie per resource UUID (see
|
|
33
|
+
* cookies.ts) so browser-side hooks never need to carry or send one.
|
|
34
|
+
*/
|
|
35
|
+
declare function createOnablRouteHandlers(config: OnablNextConfig & {
|
|
36
|
+
basePath?: string;
|
|
37
|
+
}): OnablRouteHandlers;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* For Server Component pages (the quote/invoice view pages) to fetch data
|
|
41
|
+
* with a token read straight off the URL (or props) — not the cookie, since
|
|
42
|
+
* a Server Component can't set cookies itself. Use together with
|
|
43
|
+
* `setAccessCookieOn` in a Route Handler the page's first client-side
|
|
44
|
+
* interaction goes through, or call `setAccessCookieOn` on a redirect
|
|
45
|
+
* response if the page needs the cookie persisted from a bare `?token=`
|
|
46
|
+
* link (e.g. an emailed invoice link opened on a device that never went
|
|
47
|
+
* through the submission flow, so no cookie exists yet).
|
|
48
|
+
*/
|
|
49
|
+
declare function createOnablServerClient(config: OnablNextConfig): OnablClient;
|
|
50
|
+
/** Persists a customer access token as the same httpOnly cookie createOnablRouteHandlers() reads — call this from a Route Handler or Server Action, never from render. */
|
|
51
|
+
declare function setAccessCookieOn(res: NextResponse, uuid: string, token: string): void;
|
|
52
|
+
|
|
53
|
+
declare function accessCookieName(uuid: string): string;
|
|
54
|
+
/** 30 days — matches the quote/invoice UUID's own practical lifetime; the API is the real source of truth on expiry, this is just how long the browser bothers to keep offering the token. */
|
|
55
|
+
declare const ACCESS_COOKIE_MAX_AGE_SECONDS: number;
|
|
56
|
+
|
|
57
|
+
export { ACCESS_COOKIE_MAX_AGE_SECONDS, type OnablNextConfig, type OnablRouteHandlers, accessCookieName, createOnablRouteHandlers, createOnablServerClient, setAccessCookieOn };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { OnablClient } from '@onabl/js';
|
|
3
|
+
|
|
4
|
+
interface OnablNextConfig {
|
|
5
|
+
handle: string;
|
|
6
|
+
secretKey: string;
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
apiVersion?: string;
|
|
9
|
+
quotePath?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface OnablRouteHandlers {
|
|
13
|
+
GET: (req: NextRequest) => Promise<Response>;
|
|
14
|
+
POST: (req: NextRequest) => Promise<Response>;
|
|
15
|
+
PATCH: (req: NextRequest) => Promise<Response>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Route-handler factory for the same-origin proxy every @onabl/react hook
|
|
19
|
+
* calls (see packages/react/src/context.tsx) — the reference implementation
|
|
20
|
+
* of onabl.routes() described in docs/SDK-DESIGN.md §14.5.4. Mount the
|
|
21
|
+
* returned {GET, POST, PATCH} at a single catch-all route matching
|
|
22
|
+
* `basePath`, e.g. `app/api/onabl/[...onabl]/route.ts`:
|
|
23
|
+
*
|
|
24
|
+
* export const {GET, POST, PATCH} = createOnablRouteHandlers({
|
|
25
|
+
* handle: "acme",
|
|
26
|
+
* secretKey: process.env.ONABL_SECRET_KEY!,
|
|
27
|
+
* basePath: "/api/onabl",
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* Holds config.secretKey server-side only — it never reaches the browser.
|
|
31
|
+
* Customer access tokens (quote/invoice-scoped, not secrets in the same
|
|
32
|
+
* sense) are read from/written to an httpOnly cookie per resource UUID (see
|
|
33
|
+
* cookies.ts) so browser-side hooks never need to carry or send one.
|
|
34
|
+
*/
|
|
35
|
+
declare function createOnablRouteHandlers(config: OnablNextConfig & {
|
|
36
|
+
basePath?: string;
|
|
37
|
+
}): OnablRouteHandlers;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* For Server Component pages (the quote/invoice view pages) to fetch data
|
|
41
|
+
* with a token read straight off the URL (or props) — not the cookie, since
|
|
42
|
+
* a Server Component can't set cookies itself. Use together with
|
|
43
|
+
* `setAccessCookieOn` in a Route Handler the page's first client-side
|
|
44
|
+
* interaction goes through, or call `setAccessCookieOn` on a redirect
|
|
45
|
+
* response if the page needs the cookie persisted from a bare `?token=`
|
|
46
|
+
* link (e.g. an emailed invoice link opened on a device that never went
|
|
47
|
+
* through the submission flow, so no cookie exists yet).
|
|
48
|
+
*/
|
|
49
|
+
declare function createOnablServerClient(config: OnablNextConfig): OnablClient;
|
|
50
|
+
/** Persists a customer access token as the same httpOnly cookie createOnablRouteHandlers() reads — call this from a Route Handler or Server Action, never from render. */
|
|
51
|
+
declare function setAccessCookieOn(res: NextResponse, uuid: string, token: string): void;
|
|
52
|
+
|
|
53
|
+
declare function accessCookieName(uuid: string): string;
|
|
54
|
+
/** 30 days — matches the quote/invoice UUID's own practical lifetime; the API is the real source of truth on expiry, this is just how long the browser bothers to keep offering the token. */
|
|
55
|
+
declare const ACCESS_COOKIE_MAX_AGE_SECONDS: number;
|
|
56
|
+
|
|
57
|
+
export { ACCESS_COOKIE_MAX_AGE_SECONDS, type OnablNextConfig, type OnablRouteHandlers, accessCookieName, createOnablRouteHandlers, createOnablServerClient, setAccessCookieOn };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ACCESS_COOKIE_MAX_AGE_SECONDS: () => ACCESS_COOKIE_MAX_AGE_SECONDS,
|
|
24
|
+
accessCookieName: () => accessCookieName,
|
|
25
|
+
createOnablRouteHandlers: () => createOnablRouteHandlers,
|
|
26
|
+
createOnablServerClient: () => createOnablServerClient,
|
|
27
|
+
setAccessCookieOn: () => setAccessCookieOn
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/route-handlers.ts
|
|
32
|
+
var import_js2 = require("@onabl/js");
|
|
33
|
+
var import_server = require("next/server");
|
|
34
|
+
|
|
35
|
+
// src/config.ts
|
|
36
|
+
var import_js = require("@onabl/js");
|
|
37
|
+
function buildClient(config) {
|
|
38
|
+
return (0, import_js.createOnablClient)(config);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/cookies.ts
|
|
42
|
+
var COOKIE_PREFIX = "onabl_access_";
|
|
43
|
+
function accessCookieName(uuid) {
|
|
44
|
+
return `${COOKIE_PREFIX}${uuid}`;
|
|
45
|
+
}
|
|
46
|
+
var ACCESS_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 30;
|
|
47
|
+
|
|
48
|
+
// src/route-handlers.ts
|
|
49
|
+
function errorResponse(err) {
|
|
50
|
+
if (err instanceof import_js2.OnablError) {
|
|
51
|
+
return import_server.NextResponse.json({ message: err.message }, { status: err.status ?? 400 });
|
|
52
|
+
}
|
|
53
|
+
return import_server.NextResponse.json({ message: "Unexpected error" }, { status: 500 });
|
|
54
|
+
}
|
|
55
|
+
function subpath(req, basePath) {
|
|
56
|
+
const { pathname } = req.nextUrl;
|
|
57
|
+
return pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;
|
|
58
|
+
}
|
|
59
|
+
function createOnablRouteHandlers(config) {
|
|
60
|
+
const basePath = config.basePath ?? "/api/onabl";
|
|
61
|
+
const client = buildClient(config);
|
|
62
|
+
async function handleGet(req) {
|
|
63
|
+
const path = subpath(req, basePath);
|
|
64
|
+
if (path === "/forms/validate-code") {
|
|
65
|
+
const code = req.nextUrl.searchParams.get("code") ?? "";
|
|
66
|
+
const result = await client.forms.validateDiscountCode(code);
|
|
67
|
+
return import_server.NextResponse.json(result);
|
|
68
|
+
}
|
|
69
|
+
return import_server.NextResponse.json({ message: "Not found" }, { status: 404 });
|
|
70
|
+
}
|
|
71
|
+
async function handlePost(req) {
|
|
72
|
+
const path = subpath(req, basePath);
|
|
73
|
+
if (path === "/submissions") {
|
|
74
|
+
const body = await req.json();
|
|
75
|
+
const created = await client.submissions.create(body.answers, {
|
|
76
|
+
consentGiven: body.consentGiven,
|
|
77
|
+
discountCode: body.discountCode
|
|
78
|
+
});
|
|
79
|
+
const res = import_server.NextResponse.json({ quoteId: created.quoteId, quoteUrl: created.quoteUrl });
|
|
80
|
+
res.cookies.set(accessCookieName(created.quoteId), created.token, {
|
|
81
|
+
httpOnly: true,
|
|
82
|
+
sameSite: "lax",
|
|
83
|
+
secure: true,
|
|
84
|
+
maxAge: ACCESS_COOKIE_MAX_AGE_SECONDS,
|
|
85
|
+
path: "/"
|
|
86
|
+
});
|
|
87
|
+
return res;
|
|
88
|
+
}
|
|
89
|
+
if (path === "/storage/presign") {
|
|
90
|
+
const body = await req.json();
|
|
91
|
+
const token = req.cookies.get(accessCookieName(body.resourceId))?.value;
|
|
92
|
+
if (!token) return import_server.NextResponse.json({ message: "No access token for this resource" }, { status: 401 });
|
|
93
|
+
const result = await client.storage.presignPop(body.resourceId, body.filename, body.contentType, body.fileSize, { token });
|
|
94
|
+
return import_server.NextResponse.json(result);
|
|
95
|
+
}
|
|
96
|
+
const acceptMatch = /^\/quotes\/([^/]+)\/accept$/.exec(path);
|
|
97
|
+
if (acceptMatch) {
|
|
98
|
+
const uuid = acceptMatch[1];
|
|
99
|
+
const token = req.cookies.get(accessCookieName(uuid))?.value;
|
|
100
|
+
if (!token) return import_server.NextResponse.json({ message: "No access token for this quote" }, { status: 401 });
|
|
101
|
+
await client.quotes.accept(uuid, { token });
|
|
102
|
+
return import_server.NextResponse.json({ accepted: true });
|
|
103
|
+
}
|
|
104
|
+
const declineMatch = /^\/quotes\/([^/]+)\/decline$/.exec(path);
|
|
105
|
+
if (declineMatch) {
|
|
106
|
+
const uuid = declineMatch[1];
|
|
107
|
+
const token = req.cookies.get(accessCookieName(uuid))?.value;
|
|
108
|
+
if (!token) return import_server.NextResponse.json({ message: "No access token for this quote" }, { status: 401 });
|
|
109
|
+
const body = await req.json();
|
|
110
|
+
await client.quotes.decline(uuid, body.declineReason, body.declineNote, { token });
|
|
111
|
+
return import_server.NextResponse.json({ declined: true });
|
|
112
|
+
}
|
|
113
|
+
return import_server.NextResponse.json({ message: "Not found" }, { status: 404 });
|
|
114
|
+
}
|
|
115
|
+
async function handlePatch(req) {
|
|
116
|
+
const path = subpath(req, basePath);
|
|
117
|
+
const popMatch = /^\/invoices\/([^/]+)\/pop$/.exec(path);
|
|
118
|
+
if (popMatch) {
|
|
119
|
+
const uuid = popMatch[1];
|
|
120
|
+
const token = req.cookies.get(accessCookieName(uuid))?.value;
|
|
121
|
+
if (!token) return import_server.NextResponse.json({ message: "No access token for this invoice" }, { status: 401 });
|
|
122
|
+
const body = await req.json();
|
|
123
|
+
await client.invoices.savePopUrl(uuid, body.key, body.milestone, { token });
|
|
124
|
+
return import_server.NextResponse.json({ saved: true });
|
|
125
|
+
}
|
|
126
|
+
return import_server.NextResponse.json({ message: "Not found" }, { status: 404 });
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
GET: (req) => handleGet(req).catch(errorResponse),
|
|
130
|
+
POST: (req) => handlePost(req).catch(errorResponse),
|
|
131
|
+
PATCH: (req) => handlePatch(req).catch(errorResponse)
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/server.ts
|
|
136
|
+
var import_js3 = require("@onabl/js");
|
|
137
|
+
function createOnablServerClient(config) {
|
|
138
|
+
return (0, import_js3.createOnablClient)(config);
|
|
139
|
+
}
|
|
140
|
+
function setAccessCookieOn(res, uuid, token) {
|
|
141
|
+
res.cookies.set(accessCookieName(uuid), token, {
|
|
142
|
+
httpOnly: true,
|
|
143
|
+
sameSite: "lax",
|
|
144
|
+
secure: true,
|
|
145
|
+
maxAge: ACCESS_COOKIE_MAX_AGE_SECONDS,
|
|
146
|
+
path: "/"
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
150
|
+
0 && (module.exports = {
|
|
151
|
+
ACCESS_COOKIE_MAX_AGE_SECONDS,
|
|
152
|
+
accessCookieName,
|
|
153
|
+
createOnablRouteHandlers,
|
|
154
|
+
createOnablServerClient,
|
|
155
|
+
setAccessCookieOn
|
|
156
|
+
});
|
|
157
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/route-handlers.ts","../src/config.ts","../src/cookies.ts","../src/server.ts"],"sourcesContent":["export {createOnablRouteHandlers} from \"./route-handlers\";\nexport type {OnablRouteHandlers} from \"./route-handlers\";\nexport {createOnablServerClient, setAccessCookieOn} from \"./server\";\nexport type {OnablNextConfig} from \"./config\";\nexport {accessCookieName, ACCESS_COOKIE_MAX_AGE_SECONDS} from \"./cookies\";\n","import {OnablError} from \"@onabl/js\";\nimport {NextResponse} from \"next/server\";\nimport type {NextRequest} from \"next/server\";\n\nimport {buildClient, type OnablNextConfig} from \"./config\";\nimport {accessCookieName, ACCESS_COOKIE_MAX_AGE_SECONDS} from \"./cookies\";\n\nexport interface OnablRouteHandlers {\n GET: (req: NextRequest) => Promise<Response>;\n POST: (req: NextRequest) => Promise<Response>;\n PATCH: (req: NextRequest) => Promise<Response>;\n}\n\nfunction errorResponse(err: unknown): NextResponse {\n if (err instanceof OnablError) {\n return NextResponse.json({message: err.message}, {status: err.status ?? 400});\n }\n return NextResponse.json({message: \"Unexpected error\"}, {status: 500});\n}\n\n/** Strips config.basePath from the incoming request's pathname, e.g. \"/api/onabl/quotes/q-1/accept\" -> \"/quotes/q-1/accept\". */\nfunction subpath(req: NextRequest, basePath: string): string {\n const {pathname} = req.nextUrl;\n return pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;\n}\n\n/**\n * Route-handler factory for the same-origin proxy every @onabl/react hook\n * calls (see packages/react/src/context.tsx) — the reference implementation\n * of onabl.routes() described in docs/SDK-DESIGN.md §14.5.4. Mount the\n * returned {GET, POST, PATCH} at a single catch-all route matching\n * `basePath`, e.g. `app/api/onabl/[...onabl]/route.ts`:\n *\n * export const {GET, POST, PATCH} = createOnablRouteHandlers({\n * handle: \"acme\",\n * secretKey: process.env.ONABL_SECRET_KEY!,\n * basePath: \"/api/onabl\",\n * });\n *\n * Holds config.secretKey server-side only — it never reaches the browser.\n * Customer access tokens (quote/invoice-scoped, not secrets in the same\n * sense) are read from/written to an httpOnly cookie per resource UUID (see\n * cookies.ts) so browser-side hooks never need to carry or send one.\n */\nexport function createOnablRouteHandlers(config: OnablNextConfig & {basePath?: string}): OnablRouteHandlers {\n const basePath = config.basePath ?? \"/api/onabl\";\n const client = buildClient(config);\n\n async function handleGet(req: NextRequest): Promise<Response> {\n const path = subpath(req, basePath);\n\n if (path === \"/forms/validate-code\") {\n const code = req.nextUrl.searchParams.get(\"code\") ?? \"\";\n const result = await client.forms.validateDiscountCode(code);\n return NextResponse.json(result);\n }\n\n return NextResponse.json({message: \"Not found\"}, {status: 404});\n }\n\n async function handlePost(req: NextRequest): Promise<Response> {\n const path = subpath(req, basePath);\n\n if (path === \"/submissions\") {\n const body = await req.json();\n const created = await client.submissions.create(body.answers, {\n consentGiven: body.consentGiven,\n discountCode: body.discountCode,\n });\n const res = NextResponse.json({quoteId: created.quoteId, quoteUrl: created.quoteUrl});\n res.cookies.set(accessCookieName(created.quoteId), created.token, {\n httpOnly: true,\n sameSite: \"lax\",\n secure: true,\n maxAge: ACCESS_COOKIE_MAX_AGE_SECONDS,\n path: \"/\",\n });\n return res;\n }\n\n if (path === \"/storage/presign\") {\n const body = await req.json();\n const token = req.cookies.get(accessCookieName(body.resourceId))?.value;\n if (!token) return NextResponse.json({message: \"No access token for this resource\"}, {status: 401});\n const result = await client.storage.presignPop(body.resourceId, body.filename, body.contentType, body.fileSize, {token});\n return NextResponse.json(result);\n }\n\n const acceptMatch = /^\\/quotes\\/([^/]+)\\/accept$/.exec(path);\n if (acceptMatch) {\n const uuid = acceptMatch[1]!;\n const token = req.cookies.get(accessCookieName(uuid))?.value;\n if (!token) return NextResponse.json({message: \"No access token for this quote\"}, {status: 401});\n await client.quotes.accept(uuid, {token});\n return NextResponse.json({accepted: true});\n }\n\n const declineMatch = /^\\/quotes\\/([^/]+)\\/decline$/.exec(path);\n if (declineMatch) {\n const uuid = declineMatch[1]!;\n const token = req.cookies.get(accessCookieName(uuid))?.value;\n if (!token) return NextResponse.json({message: \"No access token for this quote\"}, {status: 401});\n const body = await req.json();\n await client.quotes.decline(uuid, body.declineReason, body.declineNote, {token});\n return NextResponse.json({declined: true});\n }\n\n return NextResponse.json({message: \"Not found\"}, {status: 404});\n }\n\n async function handlePatch(req: NextRequest): Promise<Response> {\n const path = subpath(req, basePath);\n\n const popMatch = /^\\/invoices\\/([^/]+)\\/pop$/.exec(path);\n if (popMatch) {\n const uuid = popMatch[1]!;\n const token = req.cookies.get(accessCookieName(uuid))?.value;\n if (!token) return NextResponse.json({message: \"No access token for this invoice\"}, {status: 401});\n const body = await req.json();\n await client.invoices.savePopUrl(uuid, body.key, body.milestone, {token});\n return NextResponse.json({saved: true});\n }\n\n return NextResponse.json({message: \"Not found\"}, {status: 404});\n }\n\n return {\n GET: (req) => handleGet(req).catch(errorResponse),\n POST: (req) => handlePost(req).catch(errorResponse),\n PATCH: (req) => handlePatch(req).catch(errorResponse),\n };\n}\n","import {createOnablClient, type OnablClient} from \"@onabl/js\";\n\nexport interface OnablNextConfig {\n handle: string;\n secretKey: string;\n baseUrl?: string;\n apiVersion?: string;\n quotePath?: string;\n}\n\nexport function buildClient(config: OnablNextConfig): OnablClient {\n return createOnablClient(config);\n}\n","/**\n * The customer access token for a given quote/invoice UUID is stored in one\n * httpOnly cookie per resource, set by this package's own route handlers\n * (never by client JS — @onabl/react's hooks never see or send it, see\n * packages/react/src/context.tsx). This is what lets useQuoteActions/\n * usePopUpload call this proxy with no auth details of their own: the proxy\n * looks the token up itself from the cookie the browser already carries.\n */\nconst COOKIE_PREFIX = \"onabl_access_\";\n\nexport function accessCookieName(uuid: string): string {\n return `${COOKIE_PREFIX}${uuid}`;\n}\n\n/** 30 days — matches the quote/invoice UUID's own practical lifetime; the API is the real source of truth on expiry, this is just how long the browser bothers to keep offering the token. */\nexport const ACCESS_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 30;\n","import {createOnablClient, type OnablClient} from \"@onabl/js\";\nimport type {NextResponse} from \"next/server\";\n\nimport type {OnablNextConfig} from \"./config\";\nimport {accessCookieName, ACCESS_COOKIE_MAX_AGE_SECONDS} from \"./cookies\";\n\n/**\n * For Server Component pages (the quote/invoice view pages) to fetch data\n * with a token read straight off the URL (or props) — not the cookie, since\n * a Server Component can't set cookies itself. Use together with\n * `setAccessCookieOn` in a Route Handler the page's first client-side\n * interaction goes through, or call `setAccessCookieOn` on a redirect\n * response if the page needs the cookie persisted from a bare `?token=`\n * link (e.g. an emailed invoice link opened on a device that never went\n * through the submission flow, so no cookie exists yet).\n */\nexport function createOnablServerClient(config: OnablNextConfig): OnablClient {\n return createOnablClient(config);\n}\n\n/** Persists a customer access token as the same httpOnly cookie createOnablRouteHandlers() reads — call this from a Route Handler or Server Action, never from render. */\nexport function setAccessCookieOn(res: NextResponse, uuid: string, token: string): void {\n res.cookies.set(accessCookieName(uuid), token, {\n httpOnly: true,\n sameSite: \"lax\",\n secure: true,\n maxAge: ACCESS_COOKIE_MAX_AGE_SECONDS,\n path: \"/\",\n });\n}\n"],"mappings":"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","names":["import_js","import_js"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// src/route-handlers.ts
|
|
2
|
+
import { OnablError } from "@onabl/js";
|
|
3
|
+
import { NextResponse } from "next/server";
|
|
4
|
+
|
|
5
|
+
// src/config.ts
|
|
6
|
+
import { createOnablClient } from "@onabl/js";
|
|
7
|
+
function buildClient(config) {
|
|
8
|
+
return createOnablClient(config);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/cookies.ts
|
|
12
|
+
var COOKIE_PREFIX = "onabl_access_";
|
|
13
|
+
function accessCookieName(uuid) {
|
|
14
|
+
return `${COOKIE_PREFIX}${uuid}`;
|
|
15
|
+
}
|
|
16
|
+
var ACCESS_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 30;
|
|
17
|
+
|
|
18
|
+
// src/route-handlers.ts
|
|
19
|
+
function errorResponse(err) {
|
|
20
|
+
if (err instanceof OnablError) {
|
|
21
|
+
return NextResponse.json({ message: err.message }, { status: err.status ?? 400 });
|
|
22
|
+
}
|
|
23
|
+
return NextResponse.json({ message: "Unexpected error" }, { status: 500 });
|
|
24
|
+
}
|
|
25
|
+
function subpath(req, basePath) {
|
|
26
|
+
const { pathname } = req.nextUrl;
|
|
27
|
+
return pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;
|
|
28
|
+
}
|
|
29
|
+
function createOnablRouteHandlers(config) {
|
|
30
|
+
const basePath = config.basePath ?? "/api/onabl";
|
|
31
|
+
const client = buildClient(config);
|
|
32
|
+
async function handleGet(req) {
|
|
33
|
+
const path = subpath(req, basePath);
|
|
34
|
+
if (path === "/forms/validate-code") {
|
|
35
|
+
const code = req.nextUrl.searchParams.get("code") ?? "";
|
|
36
|
+
const result = await client.forms.validateDiscountCode(code);
|
|
37
|
+
return NextResponse.json(result);
|
|
38
|
+
}
|
|
39
|
+
return NextResponse.json({ message: "Not found" }, { status: 404 });
|
|
40
|
+
}
|
|
41
|
+
async function handlePost(req) {
|
|
42
|
+
const path = subpath(req, basePath);
|
|
43
|
+
if (path === "/submissions") {
|
|
44
|
+
const body = await req.json();
|
|
45
|
+
const created = await client.submissions.create(body.answers, {
|
|
46
|
+
consentGiven: body.consentGiven,
|
|
47
|
+
discountCode: body.discountCode
|
|
48
|
+
});
|
|
49
|
+
const res = NextResponse.json({ quoteId: created.quoteId, quoteUrl: created.quoteUrl });
|
|
50
|
+
res.cookies.set(accessCookieName(created.quoteId), created.token, {
|
|
51
|
+
httpOnly: true,
|
|
52
|
+
sameSite: "lax",
|
|
53
|
+
secure: true,
|
|
54
|
+
maxAge: ACCESS_COOKIE_MAX_AGE_SECONDS,
|
|
55
|
+
path: "/"
|
|
56
|
+
});
|
|
57
|
+
return res;
|
|
58
|
+
}
|
|
59
|
+
if (path === "/storage/presign") {
|
|
60
|
+
const body = await req.json();
|
|
61
|
+
const token = req.cookies.get(accessCookieName(body.resourceId))?.value;
|
|
62
|
+
if (!token) return NextResponse.json({ message: "No access token for this resource" }, { status: 401 });
|
|
63
|
+
const result = await client.storage.presignPop(body.resourceId, body.filename, body.contentType, body.fileSize, { token });
|
|
64
|
+
return NextResponse.json(result);
|
|
65
|
+
}
|
|
66
|
+
const acceptMatch = /^\/quotes\/([^/]+)\/accept$/.exec(path);
|
|
67
|
+
if (acceptMatch) {
|
|
68
|
+
const uuid = acceptMatch[1];
|
|
69
|
+
const token = req.cookies.get(accessCookieName(uuid))?.value;
|
|
70
|
+
if (!token) return NextResponse.json({ message: "No access token for this quote" }, { status: 401 });
|
|
71
|
+
await client.quotes.accept(uuid, { token });
|
|
72
|
+
return NextResponse.json({ accepted: true });
|
|
73
|
+
}
|
|
74
|
+
const declineMatch = /^\/quotes\/([^/]+)\/decline$/.exec(path);
|
|
75
|
+
if (declineMatch) {
|
|
76
|
+
const uuid = declineMatch[1];
|
|
77
|
+
const token = req.cookies.get(accessCookieName(uuid))?.value;
|
|
78
|
+
if (!token) return NextResponse.json({ message: "No access token for this quote" }, { status: 401 });
|
|
79
|
+
const body = await req.json();
|
|
80
|
+
await client.quotes.decline(uuid, body.declineReason, body.declineNote, { token });
|
|
81
|
+
return NextResponse.json({ declined: true });
|
|
82
|
+
}
|
|
83
|
+
return NextResponse.json({ message: "Not found" }, { status: 404 });
|
|
84
|
+
}
|
|
85
|
+
async function handlePatch(req) {
|
|
86
|
+
const path = subpath(req, basePath);
|
|
87
|
+
const popMatch = /^\/invoices\/([^/]+)\/pop$/.exec(path);
|
|
88
|
+
if (popMatch) {
|
|
89
|
+
const uuid = popMatch[1];
|
|
90
|
+
const token = req.cookies.get(accessCookieName(uuid))?.value;
|
|
91
|
+
if (!token) return NextResponse.json({ message: "No access token for this invoice" }, { status: 401 });
|
|
92
|
+
const body = await req.json();
|
|
93
|
+
await client.invoices.savePopUrl(uuid, body.key, body.milestone, { token });
|
|
94
|
+
return NextResponse.json({ saved: true });
|
|
95
|
+
}
|
|
96
|
+
return NextResponse.json({ message: "Not found" }, { status: 404 });
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
GET: (req) => handleGet(req).catch(errorResponse),
|
|
100
|
+
POST: (req) => handlePost(req).catch(errorResponse),
|
|
101
|
+
PATCH: (req) => handlePatch(req).catch(errorResponse)
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/server.ts
|
|
106
|
+
import { createOnablClient as createOnablClient2 } from "@onabl/js";
|
|
107
|
+
function createOnablServerClient(config) {
|
|
108
|
+
return createOnablClient2(config);
|
|
109
|
+
}
|
|
110
|
+
function setAccessCookieOn(res, uuid, token) {
|
|
111
|
+
res.cookies.set(accessCookieName(uuid), token, {
|
|
112
|
+
httpOnly: true,
|
|
113
|
+
sameSite: "lax",
|
|
114
|
+
secure: true,
|
|
115
|
+
maxAge: ACCESS_COOKIE_MAX_AGE_SECONDS,
|
|
116
|
+
path: "/"
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
export {
|
|
120
|
+
ACCESS_COOKIE_MAX_AGE_SECONDS,
|
|
121
|
+
accessCookieName,
|
|
122
|
+
createOnablRouteHandlers,
|
|
123
|
+
createOnablServerClient,
|
|
124
|
+
setAccessCookieOn
|
|
125
|
+
};
|
|
126
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/route-handlers.ts","../src/config.ts","../src/cookies.ts","../src/server.ts"],"sourcesContent":["import {OnablError} from \"@onabl/js\";\nimport {NextResponse} from \"next/server\";\nimport type {NextRequest} from \"next/server\";\n\nimport {buildClient, type OnablNextConfig} from \"./config\";\nimport {accessCookieName, ACCESS_COOKIE_MAX_AGE_SECONDS} from \"./cookies\";\n\nexport interface OnablRouteHandlers {\n GET: (req: NextRequest) => Promise<Response>;\n POST: (req: NextRequest) => Promise<Response>;\n PATCH: (req: NextRequest) => Promise<Response>;\n}\n\nfunction errorResponse(err: unknown): NextResponse {\n if (err instanceof OnablError) {\n return NextResponse.json({message: err.message}, {status: err.status ?? 400});\n }\n return NextResponse.json({message: \"Unexpected error\"}, {status: 500});\n}\n\n/** Strips config.basePath from the incoming request's pathname, e.g. \"/api/onabl/quotes/q-1/accept\" -> \"/quotes/q-1/accept\". */\nfunction subpath(req: NextRequest, basePath: string): string {\n const {pathname} = req.nextUrl;\n return pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;\n}\n\n/**\n * Route-handler factory for the same-origin proxy every @onabl/react hook\n * calls (see packages/react/src/context.tsx) — the reference implementation\n * of onabl.routes() described in docs/SDK-DESIGN.md §14.5.4. Mount the\n * returned {GET, POST, PATCH} at a single catch-all route matching\n * `basePath`, e.g. `app/api/onabl/[...onabl]/route.ts`:\n *\n * export const {GET, POST, PATCH} = createOnablRouteHandlers({\n * handle: \"acme\",\n * secretKey: process.env.ONABL_SECRET_KEY!,\n * basePath: \"/api/onabl\",\n * });\n *\n * Holds config.secretKey server-side only — it never reaches the browser.\n * Customer access tokens (quote/invoice-scoped, not secrets in the same\n * sense) are read from/written to an httpOnly cookie per resource UUID (see\n * cookies.ts) so browser-side hooks never need to carry or send one.\n */\nexport function createOnablRouteHandlers(config: OnablNextConfig & {basePath?: string}): OnablRouteHandlers {\n const basePath = config.basePath ?? \"/api/onabl\";\n const client = buildClient(config);\n\n async function handleGet(req: NextRequest): Promise<Response> {\n const path = subpath(req, basePath);\n\n if (path === \"/forms/validate-code\") {\n const code = req.nextUrl.searchParams.get(\"code\") ?? \"\";\n const result = await client.forms.validateDiscountCode(code);\n return NextResponse.json(result);\n }\n\n return NextResponse.json({message: \"Not found\"}, {status: 404});\n }\n\n async function handlePost(req: NextRequest): Promise<Response> {\n const path = subpath(req, basePath);\n\n if (path === \"/submissions\") {\n const body = await req.json();\n const created = await client.submissions.create(body.answers, {\n consentGiven: body.consentGiven,\n discountCode: body.discountCode,\n });\n const res = NextResponse.json({quoteId: created.quoteId, quoteUrl: created.quoteUrl});\n res.cookies.set(accessCookieName(created.quoteId), created.token, {\n httpOnly: true,\n sameSite: \"lax\",\n secure: true,\n maxAge: ACCESS_COOKIE_MAX_AGE_SECONDS,\n path: \"/\",\n });\n return res;\n }\n\n if (path === \"/storage/presign\") {\n const body = await req.json();\n const token = req.cookies.get(accessCookieName(body.resourceId))?.value;\n if (!token) return NextResponse.json({message: \"No access token for this resource\"}, {status: 401});\n const result = await client.storage.presignPop(body.resourceId, body.filename, body.contentType, body.fileSize, {token});\n return NextResponse.json(result);\n }\n\n const acceptMatch = /^\\/quotes\\/([^/]+)\\/accept$/.exec(path);\n if (acceptMatch) {\n const uuid = acceptMatch[1]!;\n const token = req.cookies.get(accessCookieName(uuid))?.value;\n if (!token) return NextResponse.json({message: \"No access token for this quote\"}, {status: 401});\n await client.quotes.accept(uuid, {token});\n return NextResponse.json({accepted: true});\n }\n\n const declineMatch = /^\\/quotes\\/([^/]+)\\/decline$/.exec(path);\n if (declineMatch) {\n const uuid = declineMatch[1]!;\n const token = req.cookies.get(accessCookieName(uuid))?.value;\n if (!token) return NextResponse.json({message: \"No access token for this quote\"}, {status: 401});\n const body = await req.json();\n await client.quotes.decline(uuid, body.declineReason, body.declineNote, {token});\n return NextResponse.json({declined: true});\n }\n\n return NextResponse.json({message: \"Not found\"}, {status: 404});\n }\n\n async function handlePatch(req: NextRequest): Promise<Response> {\n const path = subpath(req, basePath);\n\n const popMatch = /^\\/invoices\\/([^/]+)\\/pop$/.exec(path);\n if (popMatch) {\n const uuid = popMatch[1]!;\n const token = req.cookies.get(accessCookieName(uuid))?.value;\n if (!token) return NextResponse.json({message: \"No access token for this invoice\"}, {status: 401});\n const body = await req.json();\n await client.invoices.savePopUrl(uuid, body.key, body.milestone, {token});\n return NextResponse.json({saved: true});\n }\n\n return NextResponse.json({message: \"Not found\"}, {status: 404});\n }\n\n return {\n GET: (req) => handleGet(req).catch(errorResponse),\n POST: (req) => handlePost(req).catch(errorResponse),\n PATCH: (req) => handlePatch(req).catch(errorResponse),\n };\n}\n","import {createOnablClient, type OnablClient} from \"@onabl/js\";\n\nexport interface OnablNextConfig {\n handle: string;\n secretKey: string;\n baseUrl?: string;\n apiVersion?: string;\n quotePath?: string;\n}\n\nexport function buildClient(config: OnablNextConfig): OnablClient {\n return createOnablClient(config);\n}\n","/**\n * The customer access token for a given quote/invoice UUID is stored in one\n * httpOnly cookie per resource, set by this package's own route handlers\n * (never by client JS — @onabl/react's hooks never see or send it, see\n * packages/react/src/context.tsx). This is what lets useQuoteActions/\n * usePopUpload call this proxy with no auth details of their own: the proxy\n * looks the token up itself from the cookie the browser already carries.\n */\nconst COOKIE_PREFIX = \"onabl_access_\";\n\nexport function accessCookieName(uuid: string): string {\n return `${COOKIE_PREFIX}${uuid}`;\n}\n\n/** 30 days — matches the quote/invoice UUID's own practical lifetime; the API is the real source of truth on expiry, this is just how long the browser bothers to keep offering the token. */\nexport const ACCESS_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 30;\n","import {createOnablClient, type OnablClient} from \"@onabl/js\";\nimport type {NextResponse} from \"next/server\";\n\nimport type {OnablNextConfig} from \"./config\";\nimport {accessCookieName, ACCESS_COOKIE_MAX_AGE_SECONDS} from \"./cookies\";\n\n/**\n * For Server Component pages (the quote/invoice view pages) to fetch data\n * with a token read straight off the URL (or props) — not the cookie, since\n * a Server Component can't set cookies itself. Use together with\n * `setAccessCookieOn` in a Route Handler the page's first client-side\n * interaction goes through, or call `setAccessCookieOn` on a redirect\n * response if the page needs the cookie persisted from a bare `?token=`\n * link (e.g. an emailed invoice link opened on a device that never went\n * through the submission flow, so no cookie exists yet).\n */\nexport function createOnablServerClient(config: OnablNextConfig): OnablClient {\n return createOnablClient(config);\n}\n\n/** Persists a customer access token as the same httpOnly cookie createOnablRouteHandlers() reads — call this from a Route Handler or Server Action, never from render. */\nexport function setAccessCookieOn(res: NextResponse, uuid: string, token: string): void {\n res.cookies.set(accessCookieName(uuid), token, {\n httpOnly: true,\n sameSite: \"lax\",\n secure: true,\n maxAge: ACCESS_COOKIE_MAX_AGE_SECONDS,\n path: \"/\",\n });\n}\n"],"mappings":"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","names":["createOnablClient","createOnablClient"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@onabl/next",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Next.js integration for the onabl SDK — server-action-free route-handler factory, cookie-aware client, RSC-safe boundary. See docs/SDK-DESIGN.md.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://gitlab.com/visualcreme-studios/onabl.git",
|
|
9
|
+
"directory": "packages/next"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"module": "./dist/index.mjs",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.mjs",
|
|
24
|
+
"require": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/react": "^19",
|
|
29
|
+
"eslint": "latest",
|
|
30
|
+
"next": "latest",
|
|
31
|
+
"tsup": "^8.0.0",
|
|
32
|
+
"vitest": "^1.6.0",
|
|
33
|
+
"typescript": "^5.4.0",
|
|
34
|
+
"@onabl/typescript-config": "0.0.1",
|
|
35
|
+
"@onabl/eslint-config": "0.0.1"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@onabl/js": "0.1.0",
|
|
39
|
+
"@onabl/react": "0.1.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"next": "^16",
|
|
43
|
+
"react": "^19"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup",
|
|
47
|
+
"dev": "tsup --watch",
|
|
48
|
+
"lint": "eslint src",
|
|
49
|
+
"test": "vitest run"
|
|
50
|
+
}
|
|
51
|
+
}
|