@everframe/identity 0.3.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/LICENSE +21 -0
- package/NOTICE +8 -0
- package/README.md +103 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +170 -0
- package/dist/node.d.ts +13 -0
- package/dist/node.js +79 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ScriptX
|
|
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/NOTICE
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: MIT -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2026 ScriptX -->
|
|
3
|
+
|
|
4
|
+
# @everframe/identity
|
|
5
|
+
|
|
6
|
+
Mint [Everframe](https://everframe.dev) identity tokens from any runtime.
|
|
7
|
+
|
|
8
|
+
One `createIdentityHandler` call returns a standard
|
|
9
|
+
`(Request) => Promise<Response>` — which is simultaneously a Next App Router
|
|
10
|
+
handler, a Cloudflare Worker, a Vercel/Netlify Edge function, a Supabase Edge
|
|
11
|
+
Function and a Deno/Bun handler.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// app/api/everframe-identity/route.ts
|
|
15
|
+
import { createIdentityHandler } from '@everframe/identity';
|
|
16
|
+
|
|
17
|
+
const handler = createIdentityHandler({
|
|
18
|
+
secret: process.env.EVERFRAME_IDENTITY_SECRET!,
|
|
19
|
+
projectId: process.env.EVERFRAME_PROJECT_ID!,
|
|
20
|
+
resolveUser: async (req) => {
|
|
21
|
+
const user = await getSessionUser(req); // cookie, bearer, anything
|
|
22
|
+
return user ? { id: user.id, email: user.email, name: user.name } : null;
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Export for BOTH verbs. A cross-origin request carrying `Authorization` is
|
|
27
|
+
// ALWAYS preflighted with an OPTIONS request first — exporting only GET lets
|
|
28
|
+
// Next answer that preflight itself with a bare `Allow` header, so the
|
|
29
|
+
// handler's CORS headers (see "Cross-origin" below) never reach the browser
|
|
30
|
+
// and recognition silently stops working for every cross-origin caller.
|
|
31
|
+
export { handler as GET, handler as OPTIONS };
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Then point the SDK at it:
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
<EverframeProvider
|
|
38
|
+
config={{ apiKey }}
|
|
39
|
+
identity={{ endpoint: '/api/everframe-identity', key: user?.id }}
|
|
40
|
+
>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Auth is a callback, not a cookie
|
|
44
|
+
|
|
45
|
+
`resolveUser` receives the `Request`. Cookies are one implementation; a bearer
|
|
46
|
+
token is another:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
resolveUser: (req) => verifyAccessToken(req.headers.get('authorization'))
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
On the client, `headers` is re-invoked on **every** mint, so a rotating access
|
|
53
|
+
token is never captured stale:
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
identity={{
|
|
57
|
+
endpoint: 'https://api.example.com/everframe-identity',
|
|
58
|
+
key: user?.id,
|
|
59
|
+
headers: async () => ({ Authorization: `Bearer ${await getAccessToken()}` }),
|
|
60
|
+
}}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Cross-origin
|
|
64
|
+
|
|
65
|
+
Pass `allowedOrigins` to answer preflights and echo matched origins. Absent, the
|
|
66
|
+
handler is same-origin only. `'*'` throws — a wildcard origin on an endpoint
|
|
67
|
+
that returns identity tokens would expose them to any site.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
createIdentityHandler({ …, allowedOrigins: ['https://app.example.com'] })
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Node
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { toNodeHandler } from '@everframe/identity/node';
|
|
77
|
+
const nodeHandler = (req, res) => void toNodeHandler(handler)(req, res);
|
|
78
|
+
|
|
79
|
+
// Register for BOTH verbs — or use app.all(...) — for the same reason as the
|
|
80
|
+
// Next example above: a cross-origin request carrying `Authorization` is
|
|
81
|
+
// ALWAYS preflighted with OPTIONS first. Registering only `.get(...)` means
|
|
82
|
+
// Express answers OPTIONS with a 404 before it ever reaches the handler, so
|
|
83
|
+
// its CORS headers never appear and recognition silently stops working for
|
|
84
|
+
// every cross-origin caller.
|
|
85
|
+
app.get('/api/everframe-identity', nodeHandler);
|
|
86
|
+
app.options('/api/everframe-identity', nodeHandler);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Responses
|
|
90
|
+
|
|
91
|
+
| Case | Response |
|
|
92
|
+
|---|---|
|
|
93
|
+
| User resolves | `200 { token, expiresAt }` (`expiresAt` is ms epoch) |
|
|
94
|
+
| Nobody signed in | `200 { token: null }` |
|
|
95
|
+
| `user.id` empty or >255 chars | `200 { token: null, reason: 'subject_too_long' }` |
|
|
96
|
+
| `resolveUser` throws | `500`, empty body |
|
|
97
|
+
|
|
98
|
+
Every response carries `Cache-Control: no-store`. A signed-out user is a normal
|
|
99
|
+
state, not an error — all `token: null` cases read as "anonymous" to the SDK.
|
|
100
|
+
|
|
101
|
+
## Licence
|
|
102
|
+
|
|
103
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** The person a token is minted for. `id` becomes `sub`. */
|
|
2
|
+
interface IdentityUser {
|
|
3
|
+
/** Stable identifier. Non-empty, <=255 chars after trimming. */
|
|
4
|
+
id: string;
|
|
5
|
+
/** Optional. Dropped when empty or over 320 chars, never an error. */
|
|
6
|
+
email?: string;
|
|
7
|
+
/** Optional. Dropped when empty or over 320 chars, never an error. */
|
|
8
|
+
name?: string;
|
|
9
|
+
}
|
|
10
|
+
/** `SUBJECT_MAX` in the server identity-token verifier. */
|
|
11
|
+
declare const SUBJECT_MAX = 255;
|
|
12
|
+
/** `CLAIM_MAX` in the server identity-token verifier. */
|
|
13
|
+
declare const CLAIM_MAX = 320;
|
|
14
|
+
/** `IDENTITY_TOKEN_MAX_TTL_SEC` in the server identity-token verifier. */
|
|
15
|
+
declare const MAX_TTL_SECONDS = 600;
|
|
16
|
+
/** Well inside the ceiling, so ordinary clock drift never approaches it. */
|
|
17
|
+
declare const DEFAULT_TTL_SECONDS = 300;
|
|
18
|
+
|
|
19
|
+
interface MintOptions {
|
|
20
|
+
secret: string;
|
|
21
|
+
projectId: string;
|
|
22
|
+
user: IdentityUser;
|
|
23
|
+
ttlSeconds?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Test-only clock injection. Production callers omit it.
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
now?: Date;
|
|
29
|
+
}
|
|
30
|
+
declare function mintIdentityToken(opts: MintOptions): Promise<string>;
|
|
31
|
+
|
|
32
|
+
type ResolveUser = (req: Request) => IdentityUser | null | Promise<IdentityUser | null>;
|
|
33
|
+
interface HandlerOptions {
|
|
34
|
+
secret: string;
|
|
35
|
+
projectId: string;
|
|
36
|
+
resolveUser: ResolveUser;
|
|
37
|
+
ttlSeconds?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Exact-match origin allowlist. ABSENT means no CORS headers at all —
|
|
40
|
+
* same-origin only, which is the safe default. '*' throws.
|
|
41
|
+
*/
|
|
42
|
+
allowedOrigins?: string[];
|
|
43
|
+
/** Only meaningful with `allowedOrigins`. The cookie case. */
|
|
44
|
+
allowCredentials?: boolean;
|
|
45
|
+
}
|
|
46
|
+
declare function createIdentityHandler(opts: HandlerOptions): (req: Request) => Promise<Response>;
|
|
47
|
+
|
|
48
|
+
export { CLAIM_MAX, DEFAULT_TTL_SECONDS, type HandlerOptions, type IdentityUser, MAX_TTL_SECONDS, type MintOptions, type ResolveUser, SUBJECT_MAX, createIdentityHandler, mintIdentityToken };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { SignJWT } from 'jose';
|
|
2
|
+
|
|
3
|
+
// src/mint.ts
|
|
4
|
+
|
|
5
|
+
// src/types.ts
|
|
6
|
+
var SUBJECT_MAX = 255;
|
|
7
|
+
var CLAIM_MAX = 320;
|
|
8
|
+
var MAX_TTL_SECONDS = 600;
|
|
9
|
+
var DEFAULT_TTL_SECONDS = 300;
|
|
10
|
+
var MIN_SECRET_LENGTH = 32;
|
|
11
|
+
|
|
12
|
+
// src/mint.ts
|
|
13
|
+
function assertValidSecret(secret) {
|
|
14
|
+
if (typeof secret !== "string" || secret.trim().length < MIN_SECRET_LENGTH) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`@everframe/identity: secret must be a string of at least ${MIN_SECRET_LENGTH} characters. Generate one in Project settings -> User recognition and read it from your environment.`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function assertValidProjectId(projectId) {
|
|
21
|
+
if (typeof projectId !== "string" || projectId.trim() === "") {
|
|
22
|
+
throw new Error("@everframe/identity: projectId must be a non-empty string.");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function assertValidTtl(ttlSeconds) {
|
|
26
|
+
if (!Number.isInteger(ttlSeconds) || ttlSeconds <= 0 || ttlSeconds > MAX_TTL_SECONDS) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`@everframe/identity: ttlSeconds must be a positive integer <= ${MAX_TTL_SECONDS}. Everframe rejects longer-lived tokens as ttl_too_long.`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function optionalClaim(value) {
|
|
33
|
+
if (typeof value !== "string") return void 0;
|
|
34
|
+
const trimmed = value.trim();
|
|
35
|
+
if (trimmed === "" || trimmed.length > CLAIM_MAX) return void 0;
|
|
36
|
+
return trimmed;
|
|
37
|
+
}
|
|
38
|
+
function normalizeUser(user) {
|
|
39
|
+
const sub = typeof user?.id === "string" ? user.id.trim() : "";
|
|
40
|
+
if (sub === "" || sub.length > SUBJECT_MAX) {
|
|
41
|
+
throw new SubjectError(
|
|
42
|
+
`@everframe/identity: user.id must be a non-empty string of at most ${SUBJECT_MAX} characters.`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return { sub, email: optionalClaim(user.email), name: optionalClaim(user.name) };
|
|
46
|
+
}
|
|
47
|
+
var SubjectError = class extends Error {
|
|
48
|
+
reason = "subject_too_long";
|
|
49
|
+
};
|
|
50
|
+
async function mintIdentityToken(opts) {
|
|
51
|
+
const ttlSeconds = opts.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
52
|
+
assertValidSecret(opts.secret);
|
|
53
|
+
assertValidProjectId(opts.projectId);
|
|
54
|
+
assertValidTtl(ttlSeconds);
|
|
55
|
+
const { sub, email, name } = normalizeUser(opts.user);
|
|
56
|
+
const iat = Math.floor((opts.now?.getTime() ?? Date.now()) / 1e3);
|
|
57
|
+
return await new SignJWT({
|
|
58
|
+
...email !== void 0 ? { email } : {},
|
|
59
|
+
...name !== void 0 ? { name } : {}
|
|
60
|
+
}).setProtectedHeader({ alg: "HS256" }).setSubject(sub).setAudience(opts.projectId).setIssuedAt(iat).setExpirationTime(iat + ttlSeconds).sign(new TextEncoder().encode(opts.secret));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/cors.ts
|
|
64
|
+
var ALLOWED_METHODS = "GET, POST, OPTIONS";
|
|
65
|
+
var ALLOWED_HEADERS = "authorization, content-type";
|
|
66
|
+
var MAX_AGE = "600";
|
|
67
|
+
function isPreflight(req) {
|
|
68
|
+
return req.method === "OPTIONS" && req.headers.has("access-control-request-method");
|
|
69
|
+
}
|
|
70
|
+
function corsHeadersFor(req, allowedOrigins, allowCredentials) {
|
|
71
|
+
if (!allowedOrigins || allowedOrigins.length === 0) return null;
|
|
72
|
+
const headers = new Headers({ vary: "Origin" });
|
|
73
|
+
const origin = req.headers.get("origin");
|
|
74
|
+
if (origin === null || !allowedOrigins.includes(origin)) return headers;
|
|
75
|
+
headers.set("access-control-allow-origin", origin);
|
|
76
|
+
headers.set("access-control-allow-methods", ALLOWED_METHODS);
|
|
77
|
+
headers.set("access-control-allow-headers", ALLOWED_HEADERS);
|
|
78
|
+
headers.set("access-control-max-age", MAX_AGE);
|
|
79
|
+
if (allowCredentials) headers.set("access-control-allow-credentials", "true");
|
|
80
|
+
return headers;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/handler.ts
|
|
84
|
+
function baseHeaders() {
|
|
85
|
+
return new Headers({ "cache-control": "no-store" });
|
|
86
|
+
}
|
|
87
|
+
function json(body, status, headers) {
|
|
88
|
+
headers.set("content-type", "application/json");
|
|
89
|
+
return new Response(JSON.stringify(body), { status, headers });
|
|
90
|
+
}
|
|
91
|
+
function createIdentityHandler(opts) {
|
|
92
|
+
const ttlSeconds = opts.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
93
|
+
assertValidSecret(opts.secret);
|
|
94
|
+
assertValidProjectId(opts.projectId);
|
|
95
|
+
assertValidTtl(ttlSeconds);
|
|
96
|
+
assertValidOrigins(opts.allowedOrigins);
|
|
97
|
+
return async function identityHandler(req) {
|
|
98
|
+
const headers = baseHeaders();
|
|
99
|
+
const cors = corsHeadersFor(req, opts.allowedOrigins, opts.allowCredentials === true);
|
|
100
|
+
if (cors) for (const [k, v] of cors) headers.set(k, v);
|
|
101
|
+
if (cors && isPreflight(req)) {
|
|
102
|
+
return new Response(null, { status: 204, headers });
|
|
103
|
+
}
|
|
104
|
+
if (req.method !== "GET" && req.method !== "POST") {
|
|
105
|
+
return new Response(null, { status: 405, headers });
|
|
106
|
+
}
|
|
107
|
+
let user;
|
|
108
|
+
try {
|
|
109
|
+
user = await opts.resolveUser(req);
|
|
110
|
+
} catch {
|
|
111
|
+
return new Response(null, { status: 500, headers });
|
|
112
|
+
}
|
|
113
|
+
if (!user) return json({ token: null }, 200, headers);
|
|
114
|
+
try {
|
|
115
|
+
normalizeUser(user);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
if (err instanceof SubjectError) {
|
|
118
|
+
return json({ token: null, reason: err.reason }, 200, headers);
|
|
119
|
+
}
|
|
120
|
+
return new Response(null, { status: 500, headers });
|
|
121
|
+
}
|
|
122
|
+
const now = /* @__PURE__ */ new Date();
|
|
123
|
+
const token = await mintIdentityToken({
|
|
124
|
+
secret: opts.secret,
|
|
125
|
+
projectId: opts.projectId,
|
|
126
|
+
user,
|
|
127
|
+
ttlSeconds,
|
|
128
|
+
now
|
|
129
|
+
});
|
|
130
|
+
return json({ token, expiresAt: now.getTime() + ttlSeconds * 1e3 }, 200, headers);
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function assertValidOrigins(allowedOrigins) {
|
|
134
|
+
if (allowedOrigins === void 0) return;
|
|
135
|
+
if (!Array.isArray(allowedOrigins)) {
|
|
136
|
+
throw new Error("@everframe/identity: allowedOrigins must be an array of origin strings.");
|
|
137
|
+
}
|
|
138
|
+
if (allowedOrigins.includes("*")) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
"@everframe/identity: allowedOrigins cannot contain '*'. A wildcard origin combined with credentials is rejected by browsers and would expose identity tokens to any site. List your origins explicitly."
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
for (const entry of allowedOrigins) {
|
|
144
|
+
if (entry.trim().toLowerCase() === "null") {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`@everframe/identity: allowedOrigins cannot contain "${entry}". The string "null" is the opaque-origin placeholder shared by sandboxed iframes, data: URLs, file: pages, and some redirect flows \u2014 it does not identify a trusted caller, so allowlisting it would let any of those unrelated contexts read identity tokens. List real origins instead.`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
let url;
|
|
150
|
+
try {
|
|
151
|
+
url = new URL(entry);
|
|
152
|
+
} catch {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`@everframe/identity: allowedOrigins entry "${entry}" is not a valid URL. Expected a bare origin like "https://app.example.com".`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`@everframe/identity: allowedOrigins entry "${entry}" must use http: or https:. Expected a bare origin like "https://app.example.com".`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (url.origin !== entry) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`@everframe/identity: allowedOrigins entry "${entry}" must be a bare origin \u2014 no path, query, fragment, or trailing slash. Expected a form like "https://app.example.com".`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export { CLAIM_MAX, DEFAULT_TTL_SECONDS, MAX_TTL_SECONDS, SUBJECT_MAX, createIdentityHandler, mintIdentityToken };
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
|
|
3
|
+
type FetchHandler = (req: Request) => Promise<Response>;
|
|
4
|
+
/**
|
|
5
|
+
* Adapt a WHATWG handler to Express, Connect, Next Pages Router, or plain
|
|
6
|
+
* node:http.
|
|
7
|
+
*
|
|
8
|
+
* const handler = toNodeHandler(createIdentityHandler({ … }));
|
|
9
|
+
* app.get('/api/everframe-identity', (req, res) => void handler(req, res));
|
|
10
|
+
*/
|
|
11
|
+
declare function toNodeHandler(handler: FetchHandler): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
12
|
+
|
|
13
|
+
export { toNodeHandler };
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// src/node.ts
|
|
2
|
+
var MAX_BODY_BYTES = 1024 * 1024;
|
|
3
|
+
var BodyTooLargeError = class extends Error {
|
|
4
|
+
};
|
|
5
|
+
async function readBody(req) {
|
|
6
|
+
if (req.method !== "POST") return void 0;
|
|
7
|
+
const chunks = [];
|
|
8
|
+
let total = 0;
|
|
9
|
+
for await (const chunk of req) {
|
|
10
|
+
const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : new Uint8Array(chunk);
|
|
11
|
+
total += bytes.byteLength;
|
|
12
|
+
if (total > MAX_BODY_BYTES) {
|
|
13
|
+
throw new BodyTooLargeError();
|
|
14
|
+
}
|
|
15
|
+
chunks.push(bytes);
|
|
16
|
+
}
|
|
17
|
+
if (chunks.length === 0) return void 0;
|
|
18
|
+
const out = new Uint8Array(total);
|
|
19
|
+
let offset = 0;
|
|
20
|
+
for (const chunk of chunks) {
|
|
21
|
+
out.set(chunk, offset);
|
|
22
|
+
offset += chunk.byteLength;
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
function toNodeHandler(handler) {
|
|
27
|
+
return async function nodeHandler(req, res) {
|
|
28
|
+
try {
|
|
29
|
+
const host = req.headers.host ?? "localhost";
|
|
30
|
+
let url;
|
|
31
|
+
try {
|
|
32
|
+
url = new URL(req.url ?? "/", `http://${host}`);
|
|
33
|
+
} catch {
|
|
34
|
+
url = new URL(req.url ?? "/", "http://localhost");
|
|
35
|
+
}
|
|
36
|
+
const headers = new Headers();
|
|
37
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
38
|
+
if (value === void 0) continue;
|
|
39
|
+
if (Array.isArray(value)) for (const v of value) headers.append(key, v);
|
|
40
|
+
else headers.set(key, value);
|
|
41
|
+
}
|
|
42
|
+
let body;
|
|
43
|
+
try {
|
|
44
|
+
body = await readBody(req);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (err instanceof BodyTooLargeError) {
|
|
47
|
+
if (!res.headersSent) {
|
|
48
|
+
res.statusCode = 413;
|
|
49
|
+
res.setHeader("cache-control", "no-store");
|
|
50
|
+
res.setHeader("connection", "close");
|
|
51
|
+
res.once("finish", () => req.destroy());
|
|
52
|
+
res.end();
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
const request = new Request(url, {
|
|
59
|
+
method: req.method ?? "GET",
|
|
60
|
+
headers,
|
|
61
|
+
...body ? { body } : {}
|
|
62
|
+
});
|
|
63
|
+
const response = await handler(request);
|
|
64
|
+
if (res.headersSent) return;
|
|
65
|
+
res.statusCode = response.status;
|
|
66
|
+
response.headers.forEach((value, key) => res.setHeader(key, value));
|
|
67
|
+
const text = await response.text();
|
|
68
|
+
res.end(text === "" ? void 0 : text);
|
|
69
|
+
} catch {
|
|
70
|
+
if (!res.headersSent) {
|
|
71
|
+
res.statusCode = 500;
|
|
72
|
+
res.setHeader("cache-control", "no-store");
|
|
73
|
+
res.end();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { toNodeHandler };
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@everframe/identity",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Mint Everframe identity tokens from any runtime — one handler that works as a Next route, a Cloudflare Worker, an Edge function or a Node server.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Scriptx, UAB",
|
|
7
|
+
"email": "support@everframe.dev",
|
|
8
|
+
"url": "https://everframe.dev"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"everframe",
|
|
12
|
+
"identity",
|
|
13
|
+
"jwt",
|
|
14
|
+
"authentication",
|
|
15
|
+
"bug-reporting"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://everframe.dev",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/scriptx-com/everframe/issues"
|
|
20
|
+
},
|
|
21
|
+
"type": "module",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/scriptx-com/everframe.git",
|
|
30
|
+
"directory": "packages/identity"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"default": "./dist/index.js"
|
|
39
|
+
},
|
|
40
|
+
"./node": {
|
|
41
|
+
"types": "./dist/node.d.ts",
|
|
42
|
+
"default": "./dist/node.js"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist",
|
|
47
|
+
"LICENSE",
|
|
48
|
+
"NOTICE"
|
|
49
|
+
],
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"jose": "6.2.5"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@arethetypeswrong/cli": "^0.18.0",
|
|
55
|
+
"@types/node": "^22.0.0",
|
|
56
|
+
"publint": "^0.3.0",
|
|
57
|
+
"tsup": "^8.0.0"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsup",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"test:watch": "vitest",
|
|
63
|
+
"lint": "tsc --noEmit",
|
|
64
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
|
|
65
|
+
"check:publish": "attw --pack . --profile esm-only --ignore-rules cjs-resolves-to-esm && publint",
|
|
66
|
+
"clean": "rm -rf dist .turbo node_modules/.cache"
|
|
67
|
+
}
|
|
68
|
+
}
|