@passflow/node 0.0.0-bootstrap.0 → 0.0.0-stage

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 CHANGED
@@ -1,180 +1,3 @@
1
- # @passflow/node
1
+ # Temporary Holding Version
2
2
 
3
- Protect your own API with Passflow tokens. One import and one middleware call give you token verification, an authentication-level floor, a freshness requirement, and the exact RFC 9470 challenge the Passflow browser SDK needs to run a step-up.
4
-
5
- Requires Node.js 22.12 or newer, and Express, Fastify and Hono are optional peers, so you install only the framework you use.
6
-
7
- ```bash
8
- npm install @passflow/node
9
- ```
10
-
11
- ```js
12
- import { AAL2, createVerifier } from '@passflow/node';
13
- import { getAuth, passflowExpress } from '@passflow/node/express';
14
- import express from 'express';
15
-
16
- const verifier = createVerifier({ issuer: 'https://auth.example.com', audience: '7f3a9c21d4b8e6' });
17
- const passflow = passflowExpress(verifier);
18
-
19
- const app = express();
20
- app.get('/invoices', passflow.requireACR(AAL2), (req, res) => res.json({ sub: getAuth(req).sub }));
21
- app.post('/transfers', passflow.requireFresh(300), (_req, res) => res.json({ ok: true }));
22
- ```
23
-
24
- `audience` is the ID of a Passflow application, or a list of them. The Passflow documentation has step-by-step tutorials for Express, Fastify and Hono (`docs/tutorials/node-backend.md`) and a runnable API in `examples/resource-server`.
25
-
26
- ## Use it with a framework
27
-
28
- ### Express
29
-
30
- `passflowExpress(verifier)` returns `authenticate`, `requireACR(min)` and `requireFresh(seconds)`, and the last two work alone without `authenticate` in front. Claims live on `req.passflow`, which `getAuth(req)` reads, and that name cannot collide with the global `auth` type that `express-jwt` declares.
31
-
32
- ```js
33
- app.get('/me', passflow.authenticate, (req, res) => res.json({ sub: getAuth(req).sub }));
34
- ```
35
-
36
- ### Fastify
37
-
38
- Register the plugin once per instance, because it only decorates: routes opt in one at a time and public routes are untouched.
39
-
40
- ```js
41
- import { AAL2, createVerifier } from '@passflow/node';
42
- import { getAuth, passflowFastify } from '@passflow/node/fastify';
43
- import Fastify from 'fastify';
44
-
45
- const verifier = createVerifier({ issuer: 'https://auth.example.com', audience: '7f3a9c21d4b8e6' });
46
-
47
- const app = Fastify();
48
- await app.register(passflowFastify, { verifier });
49
-
50
- app.get('/invoices', { preHandler: app.passflow.requireACR(AAL2) }, async (request) => ({
51
- subject: getAuth(request).sub,
52
- }));
53
- ```
54
-
55
- A second registration throws Fastify's own `FST_ERR_DEC_ALREADY_PRESENT`, so use `createFastifyHooks(verifier)` for every further verifier. It returns the same three hooks with no plugin and no decoration.
56
-
57
- ### Hono
58
-
59
- `@passflow/node` and `@passflow/node/hono` import no Node built-ins, so they run on any runtime with WebCrypto and `fetch`.
60
-
61
- ```js
62
- import { AAL2, createVerifier } from '@passflow/node';
63
- import { getAuth, passflowHono } from '@passflow/node/hono';
64
- import { Hono } from 'hono';
65
-
66
- const passflow = passflowHono(createVerifier({ issuer: 'https://auth.example.com', audience: '7f3a9c21d4b8e6' }));
67
- const app = new Hono();
68
-
69
- app.get('/invoices', passflow.requireACR(AAL2), (c) => c.json({ subject: getAuth(c).sub }));
70
- ```
71
-
72
- ## Responses and CORS
73
-
74
- | Situation | Status | `WWW-Authenticate` |
75
- |---|---|---|
76
- | No token, or not a `Bearer` token | 401 | `Bearer` |
77
- | Invalid or expired token | 401 | `Bearer error="invalid_token"` |
78
- | Token below the required level | 401 | `Bearer error="insufficient_user_authentication", acr_values="2"` |
79
- | Authentication too old | 401 | `Bearer error="insufficient_user_authentication", max_age=300` |
80
- | Key set unavailable | 503 | none |
81
-
82
- Every rejection has `Cache-Control: no-store` and an empty body, and the middleware writes it directly, so your own error handler cannot drop the challenge. The browser SDK acts only on the step-up challenge, and the library never sends one for an authentication failure. Nothing from the token, the request or the key set URL appears in any error or response, and the package writes no logs.
83
-
84
- A browser hides `WWW-Authenticate` from a cross-origin page unless the response names it in `Access-Control-Expose-Headers`. The SDK then sees a bare 401 and cannot start the step-up, and a `*` entry does not cover the header on a credentialed request.
85
-
86
- Each adapter exports `exposeChallengeHeader`, which adds the header to your CORS options and keeps the entries you already have, under the option name that framework's middleware uses: `exposedHeaders` for `cors` and `@fastify/cors`, `exposeHeaders` for `hono/cors`.
87
-
88
- ```js
89
- import { exposeChallengeHeader } from '@passflow/node/express';
90
- import cors from 'cors';
91
-
92
- app.use(cors(exposeChallengeHeader({ origin: 'https://app.example.com', credentials: true })));
93
- ```
94
-
95
- `CHALLENGE_HEADER` and `CORS_EXPOSED_HEADERS` are exported if you build the list yourself.
96
-
97
- ## Claims, configuration and composition
98
-
99
- ### Reading the claims
100
-
101
- `getAuth(req)` throws `PassflowUsageError` when no Passflow middleware ran, which your framework turns into a 500.
102
-
103
- ```ts
104
- type PassflowClaims = {
105
- sub: string; iss: string; aud: string[]; exp: number; iat?: number;
106
- authTime?: number; // whole Unix seconds; absent when the claim is absent, not a number or beyond ±2^53
107
- acr: string; // '0' when the claim is absent or not a string
108
- amr: string[]; scopes: string[]; sid?: string; type: string;
109
- tenants: Record<string, TenantMembership>;
110
- verifiedAt: number; // the verifier's clock at verification, in Unix seconds
111
- raw: Record<string, unknown>;
112
- };
113
- ```
114
-
115
- `rolesOf(claims, tenantId)` returns the roles of the tenant's root group. Compare levels with `acrSatisfies(acr, min)`, which is exact over the whole 64-bit range and never satisfied by a required level that parses to 0. `acrLevel(acr)` returns a `number` for display only. It is exact up to 2^53, and the library never decides with it.
116
-
117
- ### Configuration
118
-
119
- ```js
120
- createVerifier({
121
- issuer: 'https://auth.example.com', // required, exact match
122
- audience: ['7f3a9c21d4b8e6', 'b81e4d07a39c52'], // required: a string or a list; the token must name at least one
123
- jwksUrl: 'https://auth.example.com/.well-known/jwks.json', // this is the default; OIDC discovery is not fetched
124
- allowInsecureJwksUrl: false, // https is required except on loopback hosts, unless this is set
125
- leewaySeconds: 30, // exp and nbf only
126
- tokenTypes: ['access'],
127
- now: () => new Date(),
128
- jwks: {
129
- maxAge: 600000, // ms; a set older than this keeps answering while it reloads in the background
130
- minRefreshInterval: 30000, // ms; at most one attempt per interval for a background reload, after a failure or for an unknown kid
131
- maxStale: 3600000, // ms; how long after the last successful fetch a stale set is trusted
132
- maxBytes: 1048576, // response body cap
133
- timeout: 5000, // ms, an integer up to 2147483647
134
- fetch, // replaces the global fetch, for proxies
135
- },
136
- });
137
- ```
138
-
139
- An invalid configuration throws `TypeError` naming the option, at startup. `requireACR('gold')` throws too, because 'gold' has no level above 0. A freshness limit must truncate to a whole number of seconds from 1 to `Number.MAX_SAFE_INTEGER`. Leaving a `jwks` option out gives the default. An explicit `0` is a value: `maxAge: 0` starts a background reload on every verification, gated by `minRefreshInterval`, and `minRefreshInterval: 0` removes the gate, both legal and both discouraged. `maxBytes: 0`, `maxStale: 0` and `timeout: 0` throw.
140
-
141
- Build one verifier per trust policy, at startup, because each owns a key cache.
142
-
143
- ### Stacking verifiers
144
-
145
- An adapter's `authenticate` always verifies the presented token and replaces any claims on the request. `requireACR` and `requireFresh` act on existing claims only when their own verifier produced that exact object. Otherwise they verify the presented token themselves. A broad verifier upstream therefore cannot let a token outside a route's issuer, audiences or token types through on its level alone:
146
-
147
- ```js
148
- app.use(broad.authenticate);
149
- app.get('/restricted', narrow.requireACR('2'), handler);
150
- ```
151
-
152
- A token outside `narrow`'s policy gets `invalid_token`, never a step-up. The identity rule is strict: two verifiers built from the same configuration re-verify each other's claims, and so does a spread or cloned claims object. A wrapper object that delegates to `verifier.verify` is a different identity for the same reason, and each case costs one more signature check and never a bypass. Keep one verifier per trust policy, and put the narrowest verifier's method directly on the route.
153
-
154
- In Fastify, the broad hook goes on an encapsulated scope and the narrow one on the route:
155
-
156
- ```js
157
- await app.register(async (scope) => {
158
- scope.addHook('preHandler', scope.passflow.authenticate);
159
- scope.get('/restricted', { preHandler: createFastifyHooks(narrowVerifier).requireACR('2') }, handler);
160
- });
161
- ```
162
-
163
- ## Key set behaviour
164
-
165
- The key set is fetched and cached by this package, with the same rules and defaults as the Go library `github.com/madappgang/passflow-go/auth`, though the Go options are `time.Duration` values and these are milliseconds.
166
-
167
- - A token's key is looked up by `kid` and the key type its algorithm requires. An unknown `kid` causes at most one refetch per `minRefreshInterval`, and a bad signature never causes one. The request with the unknown `kid` waits for that refetch, or joins one already running, and one verification never makes two fetches.
168
- - A set older than `maxAge` answers immediately and starts a background reload, unless one is running or `minRefreshInterval` has not passed since the last attempt. A request waits for a reload only when there is no set, or the set is past `maxStale`.
169
- - A reload counts only when all of it succeeds: status 200, a body within `maxBytes`, a JSON key set, and every usable key imported and checked. A usable key must be public, with no private member, RSA of at least 2048 bits with an odd exponent from 3 to 2^31 - 1, or P-256. Each key member must be canonical unpadded base64url. A key that carries private material fails the reload.
170
- - Anything short of a full success is one failed attempt, and a working set stays in place.
171
- - While reloads fail, a stale set is trusted until `maxStale` after the last successful fetch. Then every request gets a 503 until a reload succeeds.
172
- - A 503 never carries a challenge. A key rotation during an outage therefore does not look like a bad token.
173
-
174
- **Serverless and edge runtimes.** A runtime that freezes or cancels work after a response may never finish a background reload. The set is then refreshed by the waiting path once it is past `maxStale`, or by an unknown `kid`.
175
-
176
- **Rotation window.** A token signed by a key published less than `minRefreshInterval` after the previous fetch attempt is rejected as `key_unknown` until the gate opens, at most 30 seconds per instance with the defaults. Passflow signs with one key today, so only a deployment that rotates keys needs to call `await verifier.refresh()` from its rotation hook. It ignores the gate and reloads from a fetch that starts after the call, waiting out any fetch already running, so its success proves the set is newer than the call. It can therefore take up to twice `timeout`.
177
-
178
- ## License
179
-
180
- Released under the MIT license, which is separate from the Passflow server's license.
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.
package/package.json CHANGED
@@ -1,106 +1,6 @@
1
1
  {
2
2
  "name": "@passflow/node",
3
- "version": "0.0.0-bootstrap.0",
4
- "description": "Passflow authorization primitives for Node.js resource servers: token verification, ACR floors, auth_time freshness and RFC 9470 step-up challenges, with Express, Fastify and Hono adapters.",
5
- "license": "MIT",
6
- "repository": {
7
- "type": "git",
8
- "url": "git+https://github.com/MadAppGang/passflow.git",
9
- "directory": "sdk/node"
10
- },
11
- "type": "module",
12
- "sideEffects": false,
13
- "engines": {
14
- "node": ">=22.12"
15
- },
16
- "files": [
17
- "dist",
18
- "README.md",
19
- "LICENSE"
20
- ],
21
- "exports": {
22
- ".": {
23
- "import": {
24
- "types": "./dist/index.d.mts",
25
- "default": "./dist/index.mjs"
26
- },
27
- "require": {
28
- "types": "./dist/index.d.cts",
29
- "default": "./dist/index.cjs"
30
- }
31
- },
32
- "./express": {
33
- "import": {
34
- "types": "./dist/express.d.mts",
35
- "default": "./dist/express.mjs"
36
- },
37
- "require": {
38
- "types": "./dist/express.d.cts",
39
- "default": "./dist/express.cjs"
40
- }
41
- },
42
- "./fastify": {
43
- "import": {
44
- "types": "./dist/fastify.d.mts",
45
- "default": "./dist/fastify.mjs"
46
- },
47
- "require": {
48
- "types": "./dist/fastify.d.cts",
49
- "default": "./dist/fastify.cjs"
50
- }
51
- },
52
- "./hono": {
53
- "import": {
54
- "types": "./dist/hono.d.mts",
55
- "default": "./dist/hono.mjs"
56
- },
57
- "require": {
58
- "types": "./dist/hono.d.cts",
59
- "default": "./dist/hono.cjs"
60
- }
61
- },
62
- "./package.json": "./package.json"
63
- },
64
- "publishConfig": {
65
- "access": "public"
66
- },
67
- "dependencies": {
68
- "jose": "^6.2.14"
69
- },
70
- "peerDependencies": {
71
- "express": "^4.18.0 || ^5.0.0",
72
- "fastify": "^5.0.0",
73
- "hono": "^4.0.0"
74
- },
75
- "peerDependenciesMeta": {
76
- "express": {
77
- "optional": true
78
- },
79
- "fastify": {
80
- "optional": true
81
- },
82
- "hono": {
83
- "optional": true
84
- }
85
- },
86
- "devDependencies": {
87
- "@arethetypeswrong/core": "0.18.5",
88
- "@biomejs/biome": "1.9.4",
89
- "@types/express": "5.0.6",
90
- "@types/node": "^22.18.0",
91
- "express": "5.3.0",
92
- "fastify": "5.12.7",
93
- "hono": "4.13.14",
94
- "publint": "0.3.25",
95
- "tsdown": "0.23.4",
96
- "typescript": "^5.9.3",
97
- "vitest": "^3.2.4"
98
- },
99
- "scripts": {
100
- "build": "tsdown",
101
- "lint": "biome ci .",
102
- "format": "biome format . --write",
103
- "typecheck": "tsc --noEmit",
104
- "test": "vitest run"
105
- }
106
- }
3
+ "version": "0.0.0-stage",
4
+ "stub": true,
5
+ "description": "Temporary package placeholder for staged publishing"
6
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 MadAppGang Pty Ltd
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.
@@ -1,51 +0,0 @@
1
- /** The `passflow_tm` wire shape, camelCased. `tenantRoles` is decoded for raw access; `rolesOf` does not read it. */
2
- type TenantMembership = {
3
- tenantId: string;
4
- tenantName?: string;
5
- tenantRoles?: string[];
6
- rootGroupId?: string;
7
- groups?: Record<string, string[]>;
8
- groupNames?: Record<string, string>;
9
- };
10
- type PassflowClaims = {
11
- sub: string;
12
- iss: string;
13
- aud: string[];
14
- exp: number;
15
- iat?: number;
16
- /** Whole Unix seconds. Absent when the claim is absent, not a number or beyond ±2^53. */
17
- authTime?: number;
18
- /** `'0'` when the claim is absent or not a string. */
19
- acr: string;
20
- amr: string[];
21
- scopes: string[];
22
- sid?: string;
23
- type: string;
24
- tenants: Record<string, TenantMembership>;
25
- /** The verifier's clock at verification, in Unix seconds. `requireFresh` measures from it. */
26
- verifiedAt: number;
27
- raw: Record<string, unknown>;
28
- };
29
- /**
30
- * The structural part of a verifier. Declared here, with `verifiedBy`, so that provenance needs nothing
31
- * from the verifier module.
32
- */
33
- interface Verifier {
34
- verify(token: string): Promise<PassflowClaims>;
35
- refresh(): Promise<void>;
36
- }
37
- /**
38
- * True only for the exact object `verifier.verify` returned. A spread or clone loses provenance, which
39
- * costs one more verification and never a bypass. So does any wrapper or decorator object that delegates
40
- * to `verifier.verify`: it is a different identity, and this verifier's adapter re-verifies claims it
41
- * returns. Two verifiers built from the same configuration are different identities too.
42
- */
43
- declare function verifiedBy(claims: PassflowClaims, verifier: Verifier): boolean;
44
- /**
45
- * The roles a user holds in a tenant: `groups[rootGroupId]`, the rule the server's `CalcTenantRoles` and
46
- * `@passflow/core`'s `parseMembership` apply. A missing tenant, root group or map gives an empty list.
47
- * So does an empty `rootGroupId`, unlike the server: no role is granted from a group keyed by "".
48
- * `tenantRoles` is a copy the server derives from the same root group, and is not consulted.
49
- */
50
- declare function rolesOf(claims: PassflowClaims, tenantId: string): string[];
51
- export { verifiedBy as a, rolesOf as i, TenantMembership as n, Verifier as r, PassflowClaims as t };
@@ -1,51 +0,0 @@
1
- /** The `passflow_tm` wire shape, camelCased. `tenantRoles` is decoded for raw access; `rolesOf` does not read it. */
2
- type TenantMembership = {
3
- tenantId: string;
4
- tenantName?: string;
5
- tenantRoles?: string[];
6
- rootGroupId?: string;
7
- groups?: Record<string, string[]>;
8
- groupNames?: Record<string, string>;
9
- };
10
- type PassflowClaims = {
11
- sub: string;
12
- iss: string;
13
- aud: string[];
14
- exp: number;
15
- iat?: number;
16
- /** Whole Unix seconds. Absent when the claim is absent, not a number or beyond ±2^53. */
17
- authTime?: number;
18
- /** `'0'` when the claim is absent or not a string. */
19
- acr: string;
20
- amr: string[];
21
- scopes: string[];
22
- sid?: string;
23
- type: string;
24
- tenants: Record<string, TenantMembership>;
25
- /** The verifier's clock at verification, in Unix seconds. `requireFresh` measures from it. */
26
- verifiedAt: number;
27
- raw: Record<string, unknown>;
28
- };
29
- /**
30
- * The structural part of a verifier. Declared here, with `verifiedBy`, so that provenance needs nothing
31
- * from the verifier module.
32
- */
33
- interface Verifier {
34
- verify(token: string): Promise<PassflowClaims>;
35
- refresh(): Promise<void>;
36
- }
37
- /**
38
- * True only for the exact object `verifier.verify` returned. A spread or clone loses provenance, which
39
- * costs one more verification and never a bypass. So does any wrapper or decorator object that delegates
40
- * to `verifier.verify`: it is a different identity, and this verifier's adapter re-verifies claims it
41
- * returns. Two verifiers built from the same configuration are different identities too.
42
- */
43
- declare function verifiedBy(claims: PassflowClaims, verifier: Verifier): boolean;
44
- /**
45
- * The roles a user holds in a tenant: `groups[rootGroupId]`, the rule the server's `CalcTenantRoles` and
46
- * `@passflow/core`'s `parseMembership` apply. A missing tenant, root group or map gives an empty list.
47
- * So does an empty `rootGroupId`, unlike the server: no role is granted from a group keyed by "".
48
- * `tenantRoles` is a copy the server derives from the same root group, and is not consulted.
49
- */
50
- declare function rolesOf(claims: PassflowClaims, tenantId: string): string[];
51
- export { verifiedBy as a, rolesOf as i, TenantMembership as n, Verifier as r, PassflowClaims as t };