@pisti-paypiller/express 0.0.0-stage → 0.7.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pisti
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,192 @@
1
- # Temporary Holding Version
1
+ # @pisti-paypiller/express
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Pisti for Express 5, shaped like `@clerk/express`. It reads its configuration from environment
4
+ variables, so most apps need two lines:
5
+
6
+ ```ts
7
+ import { pistiMiddleware, getAuth } from "@pisti-paypiller/express";
8
+
9
+ app.use(pistiMiddleware());
10
+ app.get("/api/me", (req, res) => {
11
+ const { isAuthenticated, userId } = getAuth(req);
12
+ if (!isAuthenticated) return res.status(401).end();
13
+ res.json({ userId });
14
+ });
15
+ ```
16
+
17
+ | Export | What it does | Clerk equivalent |
18
+ | --- | --- | --- |
19
+ | `pistiMiddleware(options?)` | Verifies the token on every request and sets `req.auth`. Refreshes expired session cookies on page loads with the handshake. Never blocks. | `clerkMiddleware()` |
20
+ | `getAuth(req, { acceptsToken? })` | The auth object: `isAuthenticated`, `userId`, `sessionId`, `orgId`, `orgRole`, `orgSlug`, `orgPermissions`, `sessionClaims`, `factorVerificationAge`, `has()`, `getToken()`; or an API key, M2M or OAuth token object when accepted | `getAuth(req, { acceptsToken })` |
21
+ | `requireAuth(options?)` | 401 for signed-out requests (or a redirect for pages), 403 when `permission`, `role` or `reverification` fails. Takes `acceptsToken`. | `requireAuth()` |
22
+ | `pistiClient` | Backend API client from `PISTI_SECRET_KEY`: `await pistiClient.users.getUser(userId)` | `clerkClient` |
23
+ | `verifyWebhook(req)` from `@pisti-paypiller/express/webhooks` | Checks a signed webhook delivery and returns the event | `verifyWebhook(req)` |
24
+
25
+ Requires Node.js 22.12 or later and Express 5. It is built on
26
+ [`@pisti-paypiller/backend`](../backend/README.md), which works without Express.
27
+
28
+ ## 1. Install and configure
29
+
30
+ ```sh
31
+ npm install @pisti-paypiller/express
32
+ ```
33
+
34
+ Copy the keys from the console (your application → Home and API keys):
35
+
36
+ ```sh
37
+ PISTI_PUBLISHABLE_KEY=pk_test_… # required: where to verify tokens
38
+ PISTI_SECRET_KEY=sk_test_… # for pistiClient
39
+ PISTI_WEBHOOK_SIGNING_SECRET=whsec_… # for verifyWebhook
40
+ PISTI_AUTHORIZED_PARTIES=https://app.example.com # recommended in production
41
+ PISTI_SIGN_IN_URL=/sign-in # optional: where requireAuth() sends pages
42
+ PISTI_JWT_KEY="-----BEGIN PUBLIC KEY-----\n…" # optional: verify without fetching signing keys
43
+ ```
44
+
45
+ Each value can also be passed as an option, for example
46
+ `pistiMiddleware({ publishableKey, authorizedParties })`.
47
+
48
+ ## 2. Protect routes
49
+
50
+ ```ts
51
+ import express from "express";
52
+ import { pistiMiddleware, requireAuth, getAuth, pistiClient } from "@pisti-paypiller/express";
53
+
54
+ const app = express();
55
+ app.use(express.json());
56
+ app.use(pistiMiddleware());
57
+
58
+ // Check inside a handler
59
+ app.get("/api/feed", (req, res) => {
60
+ const { isAuthenticated, userId } = getAuth(req);
61
+ res.json(isAuthenticated ? personalFeed(userId) : publicFeed());
62
+ });
63
+
64
+ // Look up the full user with the backend API
65
+ app.get("/api/profile", requireAuth(), async (req, res) => {
66
+ res.json(await pistiClient.users.getUser(getAuth(req).userId!));
67
+ });
68
+
69
+ // Organization permissions, roles and a recent sign-in
70
+ app.post("/api/invoices", requireAuth({ permission: "invoices:write" }), createInvoice);
71
+ app.delete("/api/workspace", requireAuth({ role: "owner", reverification: "strict" }), destroy);
72
+
73
+ // Or the same checks in code
74
+ app.post("/api/payouts", (req, res) => {
75
+ const auth = getAuth(req);
76
+ if (!auth.has({ permission: "billing:manage" })) return res.sendStatus(403);
77
+ if (!auth.has({ reverification: { level: "second_factor", afterMinutes: 10 } }))
78
+ return res.status(403).json({ code: "reverification_required" });
79
+ // …
80
+ });
81
+
82
+ // Server-rendered pages: send signed-out browsers to sign-in instead of a 401
83
+ app.get("/settings", requireAuth({ signInUrl: "/sign-in" }), renderSettings);
84
+ ```
85
+
86
+ The middleware reads the token from the `__session` cookie, which `@pisti-paypiller/react` keeps fresh on
87
+ your app's origin, or from `Authorization: Bearer` for APIs on another domain
88
+ (`useAuthenticatedFetch()` in React sends it). It checks the signature, issuer, expiry, that the
89
+ token belongs to your environment and, when configured, the requesting origin. An expired or
90
+ invalid cookie leaves the request signed out; an invalid bearer token answers 401.
91
+
92
+ `requireAuth()` answers with RFC 9457 problem details: `401 signed_out`, or `403 forbidden` or
93
+ `reverification_required`. With `signInUrl` (or `PISTI_SIGN_IN_URL`), browser page requests
94
+ are redirected there with `redirect_url` set, and `@pisti-paypiller/react` sends people back after
95
+ sign-in.
96
+
97
+ Reverification presets match Clerk's: `"strict_mfa"` (both factors within 10 minutes),
98
+ `"strict"` (second factor within 10 minutes, or the first factor for users without
99
+ two-step verification), `"moderate"` (60 minutes), `"lax"` (a day), or
100
+ `{ level: "first_factor" | "second_factor" | "multi_factor", afterMinutes }`.
101
+
102
+ Session tokens last about 60 seconds, so signing out or revoking a session takes effect within a
103
+ minute. No session store, Redis or sticky sessions are needed.
104
+
105
+ ### Server-rendered pages: the handshake
106
+
107
+ Session tokens last 60 seconds and the browser SDK refreshes them while the page is open. When
108
+ a visitor returns to a server-rendered page after the cookie expired, `pistiMiddleware()`
109
+ answers the page request with a quick redirect through Pisti (Clerk's handshake) that sets a
110
+ fresh `__session` cookie and comes back to the same URL. Your route only runs once the cookie
111
+ is fresh. API requests are never redirected.
112
+
113
+ Behind a proxy, set `app.set("trust proxy", …)` (or keep the original `Host` header) so the
114
+ middleware builds the return URL on your public origin.
115
+
116
+ The middleware never rejects a request, so public routes work whatever credential a client
117
+ sends. `requireAuth()` reports a bad token (`401`) or a verification that could not run (`503`,
118
+ for example an API key without `PISTI_SECRET_KEY`) only for token types the route accepts.
119
+
120
+ ### API keys, M2M and OAuth tokens
121
+
122
+ Routes accept only session tokens unless they ask for more, like Clerk's `acceptsToken`:
123
+
124
+ ```ts
125
+ // Scripts call as a user with an API key created in <APIKeys /> (verified with PISTI_SECRET_KEY)
126
+ app.get("/api/v1/projects", requireAuth({ acceptsToken: ["session_token", "api_key"] }), (req, res) => {
127
+ const auth = getAuth(req, { acceptsToken: ["session_token", "api_key"] });
128
+ res.json(listProjects(auth.userId));
129
+ });
130
+
131
+ // Another service calls with client credentials from a confidential OAuth application
132
+ app.post("/internal/sync", requireAuth({ acceptsToken: "m2m_token" }), (req, res) => {
133
+ const { machineId } = getAuth(req, { acceptsToken: "m2m_token" });
134
+ // …
135
+ });
136
+ ```
137
+
138
+ A machine service gets its M2M token from `POST /connect/token` with
139
+ `grant_type=client_credentials`, its client ID and secret.
140
+
141
+ ## 3. Backend API
142
+
143
+ ```ts
144
+ import { pistiClient, PistiManagementError } from "@pisti-paypiller/express";
145
+
146
+ const user = await pistiClient.users.getUser(userId);
147
+ const { items, nextCursor } = await pistiClient.users.getUserList({ limit: 50, search: "alex" });
148
+ const total = await pistiClient.users.getCount();
149
+ const { items: sessions } = await pistiClient.sessions.getSessionList({ userId, status: "active" });
150
+ for await (const each of pistiClient.users.getUserListAll()) {
151
+ // …
152
+ }
153
+ await pistiClient.users.banUser(userId);
154
+ await pistiClient.organizations.createOrganizationInvitation(orgId, { email, roleId });
155
+ ```
156
+
157
+ `pistiClient` is created from `PISTI_SECRET_KEY` and `PISTI_PUBLISHABLE_KEY` the first time you
158
+ use it. See [`@pisti-paypiller/backend`](../backend/README.md#backend-api) for every resource and its errors.
159
+
160
+ ## 4. Webhooks
161
+
162
+ ```ts
163
+ import express from "express";
164
+ import { verifyWebhook } from "@pisti-paypiller/express/webhooks";
165
+
166
+ // Register before app.use(express.json()) so the body stays raw.
167
+ app.post("/api/webhooks", express.raw({ type: "application/json" }), async (req, res) => {
168
+ try {
169
+ const event = await verifyWebhook(req);
170
+ // Events are typed and thin: read the current user with the backend API.
171
+ if (event.type === "user.created") await createLocalUser(await pistiClient.users.getUser(event.data.id));
172
+ res.sendStatus(204);
173
+ } catch {
174
+ res.sendStatus(400);
175
+ }
176
+ });
177
+ ```
178
+
179
+ It checks the HMAC-SHA256 signature and the signed timestamp (five minutes' tolerance) before
180
+ parsing. Pass `{ signingSecret: [current, previous] }` while rotating. Store `event.id` to ignore
181
+ duplicates.
182
+
183
+ ## Deploying
184
+
185
+ - Set the variables above in your host's environment. Use the production environment's
186
+ `pk_live_` / `sk_live_` keys; they require HTTPS.
187
+ - Verification is stateless and works on servers, containers and serverless functions alike.
188
+ Signing keys are fetched once per process and cached.
189
+ - Never send `PISTI_SECRET_KEY` to a browser.
190
+
191
+ See `sdk.md` in the pisti-docs repository for how the pieces fit, and `pisti-demo`
192
+ in the pisti-examples repository for a working example.
@@ -0,0 +1,65 @@
1
+ import type { Request, RequestHandler } from "express";
2
+ import { type AcceptsToken, type AnyAuthObject, type AuthObject, type AuthObjectFor, type CheckAuthorizationParams, type PistiClient } from "@pisti-paypiller/backend";
3
+ declare global {
4
+ namespace Express {
5
+ interface Request {
6
+ /** Set by pistiMiddleware(). Prefer getAuth(req). */
7
+ auth?: AnyAuthObject;
8
+ }
9
+ }
10
+ }
11
+ export interface PistiMiddlewareOptions {
12
+ /** Defaults to `PISTI_PUBLISHABLE_KEY`. */
13
+ publishableKey?: string | undefined;
14
+ /** Verifies end users' API keys. Defaults to `PISTI_SECRET_KEY`. */
15
+ secretKey?: string | undefined;
16
+ /** Origins allowed to have requested the token. Defaults to `PISTI_AUTHORIZED_PARTIES` (comma-separated). */
17
+ authorizedParties?: string[] | undefined;
18
+ /** PEM public key for networkless verification. Defaults to `PISTI_JWT_KEY`. */
19
+ jwtKey?: string | undefined;
20
+ /** Session cookie written by @pisti-paypiller/react. Defaults to `__session`. */
21
+ cookieName?: string | undefined;
22
+ }
23
+ /**
24
+ * Like Clerk's `clerkMiddleware()`: verifies the session token from the `__session` cookie or an
25
+ * `Authorization: Bearer` header offline and sets `req.auth`. Bearer API keys, OAuth access tokens
26
+ * and M2M tokens are verified too; routes choose what they accept with `getAuth(req, { acceptsToken })`.
27
+ *
28
+ * When a page request arrives with an expired session cookie, it answers with Clerk's handshake:
29
+ * a redirect through Pisti that refreshes the cookie and returns to the same URL. Server-rendered
30
+ * pages therefore never see a signed-in visitor as signed out.
31
+ *
32
+ * With no options it reads `PISTI_PUBLISHABLE_KEY`, `PISTI_SECRET_KEY`, `PISTI_JWT_KEY` and
33
+ * `PISTI_AUTHORIZED_PARTIES`.
34
+ */
35
+ export declare function pistiMiddleware(options?: PistiMiddlewareOptions): RequestHandler;
36
+ /**
37
+ * The request's auth object, like Clerk's `getAuth(req)`. By default only session tokens count;
38
+ * pass `acceptsToken` to allow API keys (`"api_key"`), M2M tokens, OAuth access tokens or `"any"`.
39
+ */
40
+ export declare function getAuth(req: Request): AuthObject;
41
+ export declare function getAuth<T extends AcceptsToken>(req: Request, options: {
42
+ acceptsToken: T;
43
+ }): AuthObjectFor<T>;
44
+ export interface RequireAuthOptions extends CheckAuthorizationParams {
45
+ /**
46
+ * Where to send signed-out browsers instead of answering 401. Defaults to `PISTI_SIGN_IN_URL`
47
+ * for requests that accept HTML. The current URL is appended as `redirect_url`.
48
+ */
49
+ signInUrl?: string | undefined;
50
+ /** Which tokens may call the route. Defaults to "session_token". */
51
+ acceptsToken?: AcceptsToken | undefined;
52
+ }
53
+ /**
54
+ * Protects a route. Signed-out requests get 401 JSON, or a redirect to the sign-in page for
55
+ * browser navigations. Optional `permission`, `role` and `reverification` checks answer 403.
56
+ */
57
+ export declare function requireAuth(options?: RequireAuthOptions): RequestHandler;
58
+ /**
59
+ * A ready-to-use backend client, like Clerk's `clerkClient`, created on first use from
60
+ * `PISTI_SECRET_KEY` and `PISTI_PUBLISHABLE_KEY`: `await pistiClient.users.getUser(userId)`.
61
+ */
62
+ export declare const pistiClient: PistiClient;
63
+ export { createPistiClient, verifyToken, PistiError, PistiManagementError } from "@pisti-paypiller/backend";
64
+ export type { AuthObject, SignedInAuthObject, SignedOutAuthObject, SessionClaims, CheckAuthorizationParams, PistiClient, PistiWebhookEvent, TokenType, AcceptsToken, AnyAuthObject, AuthObjectFor, ApiKeyAuthObject, M2MAuthObject, OAuthAuthObject, } from "@pisti-paypiller/backend";
65
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAgB,OAAO,EAAE,cAAc,EAAY,MAAM,SAAS,CAAC;AAC/E,OAAO,EAOH,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,wBAAwB,EAC7B,KAAK,WAAW,EAEnB,MAAM,0BAA0B,CAAC;AAElC,OAAO,CAAC,MAAM,CAAC;IAEX,UAAU,OAAO,CAAC;QACd,UAAU,OAAO;YACb,qDAAqD;YACrD,IAAI,CAAC,EAAE,aAAa,CAAC;SACxB;KACJ;CACJ;AAED,MAAM,WAAW,sBAAsB;IACnC,2CAA2C;IAC3C,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,6GAA6G;IAC7G,iBAAiB,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IACzC,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,iFAAiF;IACjF,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC;AAkCD;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,cAAc,CA4BpF;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,OAAO,GAAG,UAAU,CAAC;AAClD,wBAAgB,OAAO,CAAC,CAAC,SAAS,YAAY,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE;IAAE,YAAY,EAAE,CAAC,CAAA;CAAE,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;AAO9G,MAAM,WAAW,kBAAmB,SAAQ,wBAAwB;IAChE;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,oEAAoE;IACpE,YAAY,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;CAC3C;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,cAAc,CA2B5E;AAID;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,WAMxB,CAAC;AAqBH,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAC5G,YAAY,EACR,UAAU,EACV,kBAAkB,EAClB,mBAAmB,EACnB,aAAa,EACb,wBAAwB,EACxB,WAAW,EACX,iBAAiB,EACjB,SAAS,EACT,YAAY,EACZ,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,eAAe,GAClB,MAAM,0BAA0B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,151 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { PistiError, acceptedTokenTypes, authenticateRequest, createPistiClient, signedOutAuthObject, unauthenticatedObject, } from "@pisti-paypiller/backend";
3
+ // Why a request's credential failed, kept for requireAuth: the middleware itself never rejects a request, so
4
+ // public routes keep working whatever credential a client sends, and a route reports a failure only for a
5
+ // token type it accepts.
6
+ const failures = new WeakMap();
7
+ /** The kind of credential a request carries, without verifying it. */
8
+ function credentialType(req) {
9
+ const authorization = req.headers.authorization;
10
+ if (!authorization?.startsWith("Bearer "))
11
+ return "session_token";
12
+ const token = authorization.slice(7).trim();
13
+ if (token.startsWith("ak_"))
14
+ return "api_key";
15
+ try {
16
+ const payload = JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64url").toString("utf8"));
17
+ return payload.ptyp === "oauth" ? "oauth_token" : payload.ptyp === "m2m" ? "m2m_token" : "session_token";
18
+ }
19
+ catch {
20
+ return "session_token";
21
+ }
22
+ }
23
+ function absoluteUrl(req) {
24
+ return `${req.protocol}://${req.get("host")}${req.originalUrl}`;
25
+ }
26
+ function fetchRequest(req) {
27
+ const headers = new Headers();
28
+ for (const [name, value] of Object.entries(req.headers)) {
29
+ if (typeof value === "string")
30
+ headers.set(name, value);
31
+ else if (Array.isArray(value))
32
+ for (const each of value)
33
+ headers.append(name, each);
34
+ }
35
+ return new globalThis.Request(absoluteUrl(req), { method: req.method === "HEAD" ? "GET" : req.method, headers });
36
+ }
37
+ /**
38
+ * Like Clerk's `clerkMiddleware()`: verifies the session token from the `__session` cookie or an
39
+ * `Authorization: Bearer` header offline and sets `req.auth`. Bearer API keys, OAuth access tokens
40
+ * and M2M tokens are verified too; routes choose what they accept with `getAuth(req, { acceptsToken })`.
41
+ *
42
+ * When a page request arrives with an expired session cookie, it answers with Clerk's handshake:
43
+ * a redirect through Pisti that refreshes the cookie and returns to the same URL. Server-rendered
44
+ * pages therefore never see a signed-in visitor as signed out.
45
+ *
46
+ * With no options it reads `PISTI_PUBLISHABLE_KEY`, `PISTI_SECRET_KEY`, `PISTI_JWT_KEY` and
47
+ * `PISTI_AUTHORIZED_PARTIES`.
48
+ */
49
+ export function pistiMiddleware(options = {}) {
50
+ // Fail at startup, not on the first request, when the key is missing or malformed.
51
+ createPistiClient(options.publishableKey ? { publishableKey: options.publishableKey } : {});
52
+ return async (req, res, next) => {
53
+ try {
54
+ const state = await authenticateRequest(fetchRequest(req), { ...options, acceptsToken: "any" });
55
+ if (state.status === "handshake") {
56
+ for (const value of state.headers.getSetCookie())
57
+ res.append("Set-Cookie", value);
58
+ res.setHeader("Cache-Control", "no-store");
59
+ res.redirect(307, state.headers.get("Location"));
60
+ return;
61
+ }
62
+ // A bad cookie is normal between refreshes; a bad bearer token is reported by routes that require auth.
63
+ if (state.reason === "token-invalid" && req.headers.authorization?.startsWith("Bearer "))
64
+ failures.set(req, {
65
+ error: new PistiError(401, state.message ?? "The token is invalid.", "invalid_token"),
66
+ tokenType: credentialType(req),
67
+ });
68
+ req.auth = state.toAuth();
69
+ next();
70
+ }
71
+ catch (error) {
72
+ // Verification could not run (Pisti unreachable, or API keys without a secret key). Public routes still
73
+ // work; routes that require auth answer 503.
74
+ failures.set(req, { error, tokenType: credentialType(req) });
75
+ req.auth = signedOutAuthObject;
76
+ next();
77
+ }
78
+ };
79
+ }
80
+ export function getAuth(req, options) {
81
+ if (!req.auth)
82
+ throw new Error("Add app.use(pistiMiddleware()) before routes that call getAuth(req).");
83
+ const types = acceptedTokenTypes(options?.acceptsToken);
84
+ return types.includes(req.auth.tokenType) ? req.auth : unauthenticatedObject(types[0] ?? "session_token");
85
+ }
86
+ /**
87
+ * Protects a route. Signed-out requests get 401 JSON, or a redirect to the sign-in page for
88
+ * browser navigations. Optional `permission`, `role` and `reverification` checks answer 403.
89
+ */
90
+ export function requireAuth(options = {}) {
91
+ return (req, res, next) => {
92
+ const acceptsToken = options.acceptsToken ?? "session_token";
93
+ const auth = getAuth(req, { acceptsToken });
94
+ const failure = failures.get(req);
95
+ if (!auth.isAuthenticated && failure && acceptedTokenTypes(acceptsToken).includes(failure.tokenType))
96
+ return sendProblem(req, res, failure.error);
97
+ if (!auth.isAuthenticated) {
98
+ const signInUrl = options.signInUrl ?? process.env.PISTI_SIGN_IN_URL;
99
+ if (signInUrl && req.method === "GET" && req.accepts(["html", "json"]) === "html") {
100
+ const origin = `${req.protocol}://${req.get("host")}`;
101
+ const target = new URL(signInUrl, origin);
102
+ target.searchParams.set("redirect_url", new URL(req.originalUrl, origin).href);
103
+ return res.redirect(307, target.origin === origin ? target.pathname + target.search : target.href);
104
+ }
105
+ return sendProblem(req, res, new PistiError(401, "Sign in to continue.", "signed_out"));
106
+ }
107
+ const { permission, role, reverification } = options;
108
+ if (auth.tokenType === "session_token") {
109
+ if ((permission || role) && !auth.has({ permission, role }))
110
+ return sendProblem(req, res, new PistiError(403, "You don’t have permission to do this.", "forbidden"));
111
+ if (reverification && !auth.has({ reverification }))
112
+ return sendProblem(req, res, new PistiError(403, "Confirm it’s you to continue.", "reverification_required"));
113
+ }
114
+ else if (permission || role || reverification)
115
+ return sendProblem(req, res, new PistiError(403, "This route needs a signed-in user.", "forbidden"));
116
+ next();
117
+ };
118
+ }
119
+ let shared;
120
+ /**
121
+ * A ready-to-use backend client, like Clerk's `clerkClient`, created on first use from
122
+ * `PISTI_SECRET_KEY` and `PISTI_PUBLISHABLE_KEY`: `await pistiClient.users.getUser(userId)`.
123
+ */
124
+ export const pistiClient = new Proxy({}, {
125
+ get(_target, property) {
126
+ shared ??= createPistiClient();
127
+ const value = Reflect.get(shared, property, shared);
128
+ return typeof value === "function" ? value.bind(shared) : value;
129
+ },
130
+ });
131
+ function sendProblem(req, res, error) {
132
+ const status = error instanceof PistiError ? error.status : 503;
133
+ const requestId = randomUUID();
134
+ res.setHeader("X-Request-ID", requestId);
135
+ res.setHeader("Cache-Control", "no-store");
136
+ if (status === 401)
137
+ res.setHeader("WWW-Authenticate", "Bearer");
138
+ res.status(status)
139
+ .type("application/problem+json")
140
+ .json({
141
+ type: "about:blank",
142
+ status,
143
+ title: status === 401 ? "Unauthorized" : status === 403 ? "Forbidden" : "Service Unavailable",
144
+ detail: error instanceof PistiError ? error.message : "Authentication is temporarily unavailable.",
145
+ instance: req.path,
146
+ requestId,
147
+ ...(error instanceof PistiError && error.code ? { code: error.code } : {}),
148
+ });
149
+ }
150
+ export { createPistiClient, verifyToken, PistiError, PistiManagementError } from "@pisti-paypiller/backend";
151
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { Request } from "express";
2
+ import { type PistiWebhookEvent, type VerifyWebhookOptions } from "@pisti-paypiller/backend";
3
+ /**
4
+ * Verifies a webhook delivery, like Clerk's `@clerk/express/webhooks`. Mount the route with
5
+ * `express.raw({ type: "application/json" })` before `express.json()`. The signing secret
6
+ * defaults to `PISTI_WEBHOOK_SIGNING_SECRET`.
7
+ *
8
+ * ```ts
9
+ * app.post("/api/webhooks", express.raw({ type: "application/json" }), async (req, res) => {
10
+ * const event = await verifyWebhook(req);
11
+ * res.sendStatus(204);
12
+ * });
13
+ * ```
14
+ */
15
+ export declare function verifyWebhook(req: Request, options?: VerifyWebhookOptions): Promise<PistiWebhookEvent>;
16
+ export type { PistiWebhookEvent, VerifyWebhookOptions } from "@pisti-paypiller/backend";
17
+ //# sourceMappingURL=webhooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAA2B,KAAK,iBAAiB,EAAE,KAAK,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAEtH;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CACzB,GAAG,EAAE,OAAO,EACZ,OAAO,CAAC,EAAE,oBAAoB,GAC/B,OAAO,CAAC,iBAAiB,CAAC,CAE5B;AAED,YAAY,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { verifyWebhook as verify } from "@pisti-paypiller/backend";
2
+ /**
3
+ * Verifies a webhook delivery, like Clerk's `@clerk/express/webhooks`. Mount the route with
4
+ * `express.raw({ type: "application/json" })` before `express.json()`. The signing secret
5
+ * defaults to `PISTI_WEBHOOK_SIGNING_SECRET`.
6
+ *
7
+ * ```ts
8
+ * app.post("/api/webhooks", express.raw({ type: "application/json" }), async (req, res) => {
9
+ * const event = await verifyWebhook(req);
10
+ * res.sendStatus(204);
11
+ * });
12
+ * ```
13
+ */
14
+ export function verifyWebhook(req, options) {
15
+ return verify({ body: req.body, headers: req.headers }, options);
16
+ }
17
+ //# sourceMappingURL=webhooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,IAAI,MAAM,EAAqD,MAAM,0BAA0B,CAAC;AAEtH;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CACzB,GAAY,EACZ,OAA8B;IAE9B,OAAO,MAAM,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC;AACrE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "@pisti-paypiller/express",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.7.1",
4
+ "description": "Pisti for Express: pistiMiddleware(), getAuth(req), requireAuth(), pistiClient and webhook verification.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/pisti-paypiller/pisti-sdks.git",
10
+ "directory": "express"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "engines": {
16
+ "node": ">=22.12.0"
17
+ },
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ },
23
+ "./webhooks": {
24
+ "types": "./dist/webhooks.d.ts",
25
+ "import": "./dist/webhooks.js"
26
+ }
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md"
31
+ ],
32
+ "sideEffects": false,
33
+ "scripts": {
34
+ "build": "tsc -p tsconfig.json",
35
+ "check": "tsc -p tsconfig.json --noEmit",
36
+ "prepack": "npm run build"
37
+ },
38
+ "dependencies": {
39
+ "@pisti-paypiller/backend": "0.7.1"
40
+ },
41
+ "peerDependencies": {
42
+ "express": "^5.0.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/express": "5.0.6",
46
+ "@types/node": "26.6.5",
47
+ "typescript": "7.0.2"
48
+ }
49
+ }