@lunora/cloudflare-access 0.0.1 → 1.0.0-alpha.2

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.
@@ -0,0 +1,134 @@
1
+ import { JWTPayload, JWTVerifyGetKey, KeyObject } from 'jose';
2
+ /**
3
+ * The claims Cloudflare Access mints into the `Cf-Access-Jwt-Assertion` JWT.
4
+ *
5
+ * Extends the standard `JWTPayload` (`iss`/`aud`/`sub`/`exp`/`iat`/…) with the
6
+ * Access-specific fields. Which optional fields are present depends on the
7
+ * caller and the Access application config. SSO users carry `email` (and
8
+ * `groups` when the policy emits them), with `sub` as the stable user id.
9
+ * Service tokens carry `common_name` and an empty `sub`; there is no `email`.
10
+ *
11
+ * Cloudflare may add further custom claims — they pass through verbatim via the
12
+ * index signature so the claims stay a faithful view of the token.
13
+ */
14
+ interface AccessClaims extends JWTPayload {
15
+ /** Service-token name. Present for non-interactive (machine) callers instead of `email`. */
16
+ common_name?: string;
17
+ /** ISO-3166-1 alpha-2 country the request was authorized from, when available. */
18
+ country?: string;
19
+ /** Verified user email. Present for interactive (SSO) callers. */
20
+ email?: string;
21
+ /** Identity-provider group memberships, when the Access policy is configured to emit them. */
22
+ groups?: string[];
23
+ /** Per-session nonce Cloudflare rotates on re-authentication. */
24
+ identity_nonce?: string;
25
+ /** Token kind, e.g. `"app"`. */
26
+ type?: string;
27
+ }
28
+ /**
29
+ * The minimal `resolveIdentity` return contract shared with `@lunora/runtime`'s
30
+ * `WorkerOptions.resolveIdentity` (`ResolvedIdentity`). Declared structurally so
31
+ * this package takes no runtime dependency on `@lunora/runtime`; the value is
32
+ * assignable to the runtime hook.
33
+ *
34
+ * `userId` becomes `ctx.auth.userId`; every other key is forwarded (server-side,
35
+ * unforgeable) into `x-lunora-identity` and surfaced via `ctx.auth.getIdentity()`.
36
+ * `exp` (JWT epoch **seconds**) drives WebSocket credential expiry — omit it and
37
+ * a live subscription socket never expires.
38
+ */
39
+ interface ResolvedIdentityLike {
40
+ /** All other claims pass through into `ctx.auth.getIdentity()`. */
41
+ [claim: string]: unknown;
42
+ /** JWT `exp` in epoch **seconds** (NOT milliseconds). Drives WS socket expiry. */
43
+ exp?: number;
44
+ /** Absolute expiry in epoch **milliseconds**. Alternative to `exp`; takes precedence in the runtime. */
45
+ expiresAtMs?: number;
46
+ /** The stable caller id. Becomes `ctx.auth.userId` and what `serverDefault(({auth}) => auth.userId)` stamps. */
47
+ userId: string;
48
+ }
49
+ /**
50
+ * The verified Access identity produced by `createAccessResolver`. A
51
+ * {@link ResolvedIdentityLike} with the commonly-used Access claims promoted to
52
+ * named, camelCased fields (so policies read `auth.identity.groups` etc.) plus
53
+ * the full raw claim set under `access` for fidelity.
54
+ */
55
+ interface ResolvedAccessIdentity extends ResolvedIdentityLike {
56
+ /** The full, verified claim set (snake_cased wire names preserved). */
57
+ access: AccessClaims;
58
+ /** Service-token name (`common_name`), for machine callers. */
59
+ commonName?: string;
60
+ /** Verified email, for SSO callers. */
61
+ email?: string;
62
+ /** IdP group memberships, when emitted by the Access policy. */
63
+ groups?: string[];
64
+ }
65
+ /**
66
+ * A key source for `verifyAccessJwt`. Either a `jose` remote/local JWKS getter,
67
+ * or a single public key (handy for tests that mint their own RS256 tokens).
68
+ * When omitted, a cached remote JWKS is built from `teamDomain`.
69
+ */
70
+ type AccessKeySet = CryptoKey | JWTVerifyGetKey | KeyObject | Uint8Array;
71
+ /** Options for `verifyAccessJwt`. */
72
+ interface VerifyAccessJwtOptions {
73
+ /**
74
+ * The Access application **AUD tag(s)** (the application audience from the
75
+ * Access app's Overview). Verification rejects a token whose `aud` does not
76
+ * include one of these — this is what scopes a token to *your* app.
77
+ */
78
+ aud: string | string[];
79
+ /** Clock-skew tolerance in **seconds** applied to `exp`/`nbf`/`iat`. Default `0`. */
80
+ clockToleranceSec?: number;
81
+ /**
82
+ * Override the verification key source. Primarily for tests; in production
83
+ * leave unset to use the cached remote JWKS derived from `teamDomain`.
84
+ */
85
+ keySet?: AccessKeySet;
86
+ /**
87
+ * Your Cloudflare Access team domain. Accepts the short team name (`acme`),
88
+ * the host (`acme.cloudflareaccess.com`), or a full URL
89
+ * (`https://acme.cloudflareaccess.com`). Determines both the expected issuer
90
+ * and the JWKS endpoint.
91
+ */
92
+ teamDomain: string;
93
+ }
94
+ /**
95
+ * Common options for the request-driven Access primitives — how to read the JWT
96
+ * off the request and what to do when verification fails. Shared by
97
+ * {@link CreateAccessResolverOptions} and `AccessAdminGateOptions`, which add
98
+ * their distinct mapping / authorization step on top.
99
+ */
100
+ interface RequestVerifyOptions extends VerifyAccessJwtOptions {
101
+ /**
102
+ * Cookie name carrying the Access JWT when the header is absent (browser
103
+ * navigations). Default `"CF_Authorization"`.
104
+ */
105
+ cookieName?: string;
106
+ /**
107
+ * Request header carrying the Access JWT. Default `"cf-access-jwt-assertion"`
108
+ * (matched case-insensitively).
109
+ */
110
+ headerName?: string;
111
+ /**
112
+ * Invoked when a token is present but fails verification (bad signature,
113
+ * wrong audience, expired, …). The caller still fails closed (resolver
114
+ * returns `null`, admin gate returns `false`); this is your hook to
115
+ * log/observe. It is **not** called when no token is present at all.
116
+ */
117
+ onError?: (error: unknown, request: Request) => void;
118
+ }
119
+ /** Options for `createAccessResolver`; extends {@link RequestVerifyOptions}. */
120
+ interface CreateAccessResolverOptions extends RequestVerifyOptions {
121
+ /**
122
+ * Remap verified claims into the resolved identity. Return an object to
123
+ * shallow-merge over the defaults; return a `userId` to override the derived
124
+ * caller id. Runs only after signature/issuer/audience/expiry are verified.
125
+ */
126
+ mapClaims?: (claims: AccessClaims) => Record<string, unknown>;
127
+ }
128
+ /**
129
+ * A `resolveIdentity`-shaped function: maps an inbound request to a verified
130
+ * identity (or `null` for anonymous). Assignable to `@lunora/runtime`'s
131
+ * `WorkerOptions.resolveIdentity`.
132
+ */
133
+ type ResolveIdentityFunction = (request: Request, env?: unknown) => (ResolvedIdentityLike | null) | Promise<ResolvedIdentityLike | null>;
134
+ export { AccessClaims as A, CreateAccessResolverOptions as C, RequestVerifyOptions as R, VerifyAccessJwtOptions as V, ResolveIdentityFunction as a, AccessKeySet as b, ResolvedAccessIdentity as c, ResolvedIdentityLike as d };
@@ -0,0 +1,134 @@
1
+ import { JWTPayload, JWTVerifyGetKey, KeyObject } from 'jose';
2
+ /**
3
+ * The claims Cloudflare Access mints into the `Cf-Access-Jwt-Assertion` JWT.
4
+ *
5
+ * Extends the standard `JWTPayload` (`iss`/`aud`/`sub`/`exp`/`iat`/…) with the
6
+ * Access-specific fields. Which optional fields are present depends on the
7
+ * caller and the Access application config. SSO users carry `email` (and
8
+ * `groups` when the policy emits them), with `sub` as the stable user id.
9
+ * Service tokens carry `common_name` and an empty `sub`; there is no `email`.
10
+ *
11
+ * Cloudflare may add further custom claims — they pass through verbatim via the
12
+ * index signature so the claims stay a faithful view of the token.
13
+ */
14
+ interface AccessClaims extends JWTPayload {
15
+ /** Service-token name. Present for non-interactive (machine) callers instead of `email`. */
16
+ common_name?: string;
17
+ /** ISO-3166-1 alpha-2 country the request was authorized from, when available. */
18
+ country?: string;
19
+ /** Verified user email. Present for interactive (SSO) callers. */
20
+ email?: string;
21
+ /** Identity-provider group memberships, when the Access policy is configured to emit them. */
22
+ groups?: string[];
23
+ /** Per-session nonce Cloudflare rotates on re-authentication. */
24
+ identity_nonce?: string;
25
+ /** Token kind, e.g. `"app"`. */
26
+ type?: string;
27
+ }
28
+ /**
29
+ * The minimal `resolveIdentity` return contract shared with `@lunora/runtime`'s
30
+ * `WorkerOptions.resolveIdentity` (`ResolvedIdentity`). Declared structurally so
31
+ * this package takes no runtime dependency on `@lunora/runtime`; the value is
32
+ * assignable to the runtime hook.
33
+ *
34
+ * `userId` becomes `ctx.auth.userId`; every other key is forwarded (server-side,
35
+ * unforgeable) into `x-lunora-identity` and surfaced via `ctx.auth.getIdentity()`.
36
+ * `exp` (JWT epoch **seconds**) drives WebSocket credential expiry — omit it and
37
+ * a live subscription socket never expires.
38
+ */
39
+ interface ResolvedIdentityLike {
40
+ /** All other claims pass through into `ctx.auth.getIdentity()`. */
41
+ [claim: string]: unknown;
42
+ /** JWT `exp` in epoch **seconds** (NOT milliseconds). Drives WS socket expiry. */
43
+ exp?: number;
44
+ /** Absolute expiry in epoch **milliseconds**. Alternative to `exp`; takes precedence in the runtime. */
45
+ expiresAtMs?: number;
46
+ /** The stable caller id. Becomes `ctx.auth.userId` and what `serverDefault(({auth}) => auth.userId)` stamps. */
47
+ userId: string;
48
+ }
49
+ /**
50
+ * The verified Access identity produced by `createAccessResolver`. A
51
+ * {@link ResolvedIdentityLike} with the commonly-used Access claims promoted to
52
+ * named, camelCased fields (so policies read `auth.identity.groups` etc.) plus
53
+ * the full raw claim set under `access` for fidelity.
54
+ */
55
+ interface ResolvedAccessIdentity extends ResolvedIdentityLike {
56
+ /** The full, verified claim set (snake_cased wire names preserved). */
57
+ access: AccessClaims;
58
+ /** Service-token name (`common_name`), for machine callers. */
59
+ commonName?: string;
60
+ /** Verified email, for SSO callers. */
61
+ email?: string;
62
+ /** IdP group memberships, when emitted by the Access policy. */
63
+ groups?: string[];
64
+ }
65
+ /**
66
+ * A key source for `verifyAccessJwt`. Either a `jose` remote/local JWKS getter,
67
+ * or a single public key (handy for tests that mint their own RS256 tokens).
68
+ * When omitted, a cached remote JWKS is built from `teamDomain`.
69
+ */
70
+ type AccessKeySet = CryptoKey | JWTVerifyGetKey | KeyObject | Uint8Array;
71
+ /** Options for `verifyAccessJwt`. */
72
+ interface VerifyAccessJwtOptions {
73
+ /**
74
+ * The Access application **AUD tag(s)** (the application audience from the
75
+ * Access app's Overview). Verification rejects a token whose `aud` does not
76
+ * include one of these — this is what scopes a token to *your* app.
77
+ */
78
+ aud: string | string[];
79
+ /** Clock-skew tolerance in **seconds** applied to `exp`/`nbf`/`iat`. Default `0`. */
80
+ clockToleranceSec?: number;
81
+ /**
82
+ * Override the verification key source. Primarily for tests; in production
83
+ * leave unset to use the cached remote JWKS derived from `teamDomain`.
84
+ */
85
+ keySet?: AccessKeySet;
86
+ /**
87
+ * Your Cloudflare Access team domain. Accepts the short team name (`acme`),
88
+ * the host (`acme.cloudflareaccess.com`), or a full URL
89
+ * (`https://acme.cloudflareaccess.com`). Determines both the expected issuer
90
+ * and the JWKS endpoint.
91
+ */
92
+ teamDomain: string;
93
+ }
94
+ /**
95
+ * Common options for the request-driven Access primitives — how to read the JWT
96
+ * off the request and what to do when verification fails. Shared by
97
+ * {@link CreateAccessResolverOptions} and `AccessAdminGateOptions`, which add
98
+ * their distinct mapping / authorization step on top.
99
+ */
100
+ interface RequestVerifyOptions extends VerifyAccessJwtOptions {
101
+ /**
102
+ * Cookie name carrying the Access JWT when the header is absent (browser
103
+ * navigations). Default `"CF_Authorization"`.
104
+ */
105
+ cookieName?: string;
106
+ /**
107
+ * Request header carrying the Access JWT. Default `"cf-access-jwt-assertion"`
108
+ * (matched case-insensitively).
109
+ */
110
+ headerName?: string;
111
+ /**
112
+ * Invoked when a token is present but fails verification (bad signature,
113
+ * wrong audience, expired, …). The caller still fails closed (resolver
114
+ * returns `null`, admin gate returns `false`); this is your hook to
115
+ * log/observe. It is **not** called when no token is present at all.
116
+ */
117
+ onError?: (error: unknown, request: Request) => void;
118
+ }
119
+ /** Options for `createAccessResolver`; extends {@link RequestVerifyOptions}. */
120
+ interface CreateAccessResolverOptions extends RequestVerifyOptions {
121
+ /**
122
+ * Remap verified claims into the resolved identity. Return an object to
123
+ * shallow-merge over the defaults; return a `userId` to override the derived
124
+ * caller id. Runs only after signature/issuer/audience/expiry are verified.
125
+ */
126
+ mapClaims?: (claims: AccessClaims) => Record<string, unknown>;
127
+ }
128
+ /**
129
+ * A `resolveIdentity`-shaped function: maps an inbound request to a verified
130
+ * identity (or `null` for anonymous). Assignable to `@lunora/runtime`'s
131
+ * `WorkerOptions.resolveIdentity`.
132
+ */
133
+ type ResolveIdentityFunction = (request: Request, env?: unknown) => (ResolvedIdentityLike | null) | Promise<ResolvedIdentityLike | null>;
134
+ export { AccessClaims as A, CreateAccessResolverOptions as C, RequestVerifyOptions as R, VerifyAccessJwtOptions as V, ResolveIdentityFunction as a, AccessKeySet as b, ResolvedAccessIdentity as c, ResolvedIdentityLike as d };
@@ -0,0 +1,52 @@
1
+ import { M as Middleware } from "./packem_shared/index.d-C7lOF4ZA.mjs";
2
+ /**
3
+ * The slice of context {@link accessRoles} reads and augments: the `auth` facade
4
+ * every Lunora ctx carries. `getIdentity()` returns the verified identity
5
+ * envelope (`createAccessResolver`'s output, including `groups`); `roles` is the
6
+ * per-request role list `rls()` unions permissions over.
7
+ */
8
+ interface AccessRolesContext {
9
+ auth?: {
10
+ getIdentity?: () => (Record<string, unknown> | null) | Promise<Record<string, unknown> | null>;
11
+ roles?: ReadonlyArray<string>;
12
+ userId?: string | null;
13
+ };
14
+ }
15
+ /** A group→role(s) lookup table, or a function returning the role(s) for one group. */
16
+ type AccessRoleMap = ((group: string) => string | string[] | undefined) | Record<string, string | string[]>;
17
+ /** Options for {@link accessRoles}. */
18
+ interface AccessRolesOptions {
19
+ /**
20
+ * Map verified Access group names to RLS role names. A table
21
+ * (`{ "idp-admins": "admin", "idp-eng": ["editor", "viewer"] }`) or a
22
+ * function; either may return one role, an array, or `undefined` to drop the
23
+ * group. Omit to use each group name verbatim as a role.
24
+ */
25
+ map?: AccessRoleMap;
26
+ /**
27
+ * Read the group list off the resolved identity. Defaults to the `groups`
28
+ * claim (`string[]`). Override when your IdP nests groups elsewhere.
29
+ */
30
+ readGroups?: (identity: Record<string, unknown>) => ReadonlyArray<string> | undefined;
31
+ }
32
+ /**
33
+ * Middleware that lifts the verified Cloudflare Access `groups` claim into
34
+ * `ctx.auth.roles` so `rls()` policies can authorize by role. Place it **before**
35
+ * `rls(...)` in the `.use(...)` chain — `rls()` reads `ctx.auth.roles` to union
36
+ * the permissions a request carries.
37
+ *
38
+ * It reads the resolved identity via `ctx.auth.getIdentity()` (the output of
39
+ * `createAccessResolver`), maps each group to role name(s), and unions them with
40
+ * any roles already on `ctx.auth.roles` (so a role set by an earlier middleware
41
+ * is preserved). When there is no identity or no groups it forwards the context
42
+ * unchanged — anonymous requests stay role-less (fail-closed under RLS).
43
+ *
44
+ * ```ts
45
+ * export const listInvoices = query
46
+ * .use(accessRoles({ map: { "idp-admins": "admin", "idp-billing": ["billing", "viewer"] } }))
47
+ * .use(rls(policies, { roles }))
48
+ * .query(async ({ ctx }) => ...);
49
+ * ```
50
+ */
51
+ declare const accessRoles: <Context extends AccessRolesContext>(options?: AccessRolesOptions) => Middleware<Context, Context>;
52
+ export { type AccessRoleMap, type AccessRolesContext, type AccessRolesOptions, accessRoles };
@@ -0,0 +1,52 @@
1
+ import { M as Middleware } from "./packem_shared/index.d-C7lOF4ZA.js";
2
+ /**
3
+ * The slice of context {@link accessRoles} reads and augments: the `auth` facade
4
+ * every Lunora ctx carries. `getIdentity()` returns the verified identity
5
+ * envelope (`createAccessResolver`'s output, including `groups`); `roles` is the
6
+ * per-request role list `rls()` unions permissions over.
7
+ */
8
+ interface AccessRolesContext {
9
+ auth?: {
10
+ getIdentity?: () => (Record<string, unknown> | null) | Promise<Record<string, unknown> | null>;
11
+ roles?: ReadonlyArray<string>;
12
+ userId?: string | null;
13
+ };
14
+ }
15
+ /** A group→role(s) lookup table, or a function returning the role(s) for one group. */
16
+ type AccessRoleMap = ((group: string) => string | string[] | undefined) | Record<string, string | string[]>;
17
+ /** Options for {@link accessRoles}. */
18
+ interface AccessRolesOptions {
19
+ /**
20
+ * Map verified Access group names to RLS role names. A table
21
+ * (`{ "idp-admins": "admin", "idp-eng": ["editor", "viewer"] }`) or a
22
+ * function; either may return one role, an array, or `undefined` to drop the
23
+ * group. Omit to use each group name verbatim as a role.
24
+ */
25
+ map?: AccessRoleMap;
26
+ /**
27
+ * Read the group list off the resolved identity. Defaults to the `groups`
28
+ * claim (`string[]`). Override when your IdP nests groups elsewhere.
29
+ */
30
+ readGroups?: (identity: Record<string, unknown>) => ReadonlyArray<string> | undefined;
31
+ }
32
+ /**
33
+ * Middleware that lifts the verified Cloudflare Access `groups` claim into
34
+ * `ctx.auth.roles` so `rls()` policies can authorize by role. Place it **before**
35
+ * `rls(...)` in the `.use(...)` chain — `rls()` reads `ctx.auth.roles` to union
36
+ * the permissions a request carries.
37
+ *
38
+ * It reads the resolved identity via `ctx.auth.getIdentity()` (the output of
39
+ * `createAccessResolver`), maps each group to role name(s), and unions them with
40
+ * any roles already on `ctx.auth.roles` (so a role set by an earlier middleware
41
+ * is preserved). When there is no identity or no groups it forwards the context
42
+ * unchanged — anonymous requests stay role-less (fail-closed under RLS).
43
+ *
44
+ * ```ts
45
+ * export const listInvoices = query
46
+ * .use(accessRoles({ map: { "idp-admins": "admin", "idp-billing": ["billing", "viewer"] } }))
47
+ * .use(rls(policies, { roles }))
48
+ * .query(async ({ ctx }) => ...);
49
+ * ```
50
+ */
51
+ declare const accessRoles: <Context extends AccessRolesContext>(options?: AccessRolesOptions) => Middleware<Context, Context>;
52
+ export { type AccessRoleMap, type AccessRolesContext, type AccessRolesOptions, accessRoles };
package/dist/roles.mjs ADDED
@@ -0,0 +1,35 @@
1
+ const defaultReadGroups = (identity) => {
2
+ const { access } = identity;
3
+ const nested = typeof access === "object" && access !== null ? access.groups : void 0;
4
+ const groups = identity["groups"] ?? nested;
5
+ return Array.isArray(groups) ? groups.filter((group) => typeof group === "string") : void 0;
6
+ };
7
+ const rolesForGroup = (group, map) => {
8
+ if (map === void 0) {
9
+ return [group];
10
+ }
11
+ const resolved = typeof map === "function" ? map(group) : map[group];
12
+ if (resolved === void 0) {
13
+ return [];
14
+ }
15
+ return Array.isArray(resolved) ? resolved : [resolved];
16
+ };
17
+ const accessRoles = (options = {}) => {
18
+ const readGroups = options.readGroups ?? defaultReadGroups;
19
+ return async ({ ctx, next }) => {
20
+ const identity = await ctx.auth?.getIdentity?.() ?? void 0;
21
+ const groups = identity ? readGroups(identity) : void 0;
22
+ if (groups === void 0 || groups.length === 0) {
23
+ return next();
24
+ }
25
+ const roles = new Set(ctx.auth?.roles);
26
+ for (const group of groups) {
27
+ for (const role of rolesForGroup(group, options.map)) {
28
+ roles.add(role);
29
+ }
30
+ }
31
+ return next({ ctx: { auth: { ...ctx.auth, roles: [...roles] } } });
32
+ };
33
+ };
34
+
35
+ export { accessRoles };
package/package.json CHANGED
@@ -1,10 +1,70 @@
1
1
  {
2
2
  "name": "@lunora/cloudflare-access",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for @lunora/cloudflare-access",
3
+ "version": "1.0.0-alpha.2",
4
+ "description": "Cloudflare Access (Zero Trust) identity for Lunora — verify the Cf-Access-Jwt-Assertion JWT against your team JWKS and feed the verified identity into ctx.auth / RLS via a resolveIdentity adapter",
5
5
  "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
10
- }
6
+ "access",
7
+ "cloudflare",
8
+ "cloudflare-one",
9
+ "jwt",
10
+ "lunora",
11
+ "workers",
12
+ "zero-trust"
13
+ ],
14
+ "homepage": "https://lunora.sh",
15
+ "bugs": "https://github.com/anolilab/lunora/issues",
16
+ "license": "FSL-1.1-Apache-2.0",
17
+ "author": {
18
+ "name": "Daniel Bannert",
19
+ "email": "d.bannert@anolilab.de"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/anolilab/lunora.git",
24
+ "directory": "packages/cloudflare-access"
25
+ },
26
+ "files": [
27
+ "./dist",
28
+ "README.md",
29
+ "LICENSE.md",
30
+ "__assets__"
31
+ ],
32
+ "type": "module",
33
+ "sideEffects": false,
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "import": "./dist/index.mjs"
38
+ },
39
+ "./admin": {
40
+ "types": "./dist/admin.d.ts",
41
+ "import": "./dist/admin.mjs"
42
+ },
43
+ "./context": {
44
+ "types": "./dist/context.d.ts",
45
+ "import": "./dist/context.mjs"
46
+ },
47
+ "./roles": {
48
+ "types": "./dist/roles.d.ts",
49
+ "import": "./dist/roles.mjs"
50
+ },
51
+ "./package.json": "./package.json"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
56
+ "dependencies": {
57
+ "jose": "^6.1.0"
58
+ },
59
+ "peerDependencies": {
60
+ "@lunora/server": "1.0.0-alpha.8"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "@lunora/server": {
64
+ "optional": true
65
+ }
66
+ },
67
+ "engines": {
68
+ "node": "^22.15.0 || >=24.11.0"
69
+ }
70
+ }