@poly-x/sdk 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/dist/index.cjs +172 -0
- package/dist/index.d.cts +88 -0
- package/dist/index.d.mts +88 -0
- package/dist/index.mjs +162 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @poly-x/sdk
|
|
2
|
+
|
|
3
|
+
PolyX for your **Node backend** — the server-side client that verifies a caller's
|
|
4
|
+
session and tells you who they are. Uses your `sk_…` secret key (never a
|
|
5
|
+
publishable key; it throws if you pass one).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @poly-x/sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createPolyX } from "@poly-x/sdk";
|
|
17
|
+
|
|
18
|
+
const polyx = createPolyX({ secretKey: process.env.POLYX_SECRET_KEY });
|
|
19
|
+
|
|
20
|
+
// Introspect an access token → typed authorization context
|
|
21
|
+
const ctx = await polyx.auth.introspect(accessToken);
|
|
22
|
+
if (ctx.active) {
|
|
23
|
+
console.log(ctx.userId, ctx.organizationId, ctx.roles);
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Guard Express/Connect routes (no Express dependency — the middleware is typed
|
|
28
|
+
structurally):
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
app.get("/me", polyx.express.requireUser(), (req, res) => {
|
|
32
|
+
res.json(req.polyUser); // { userId, organizationId, role, roles, … }
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or use the framework-agnostic guard directly:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const user = await polyx.requireUser(request); // any { headers } request
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
All guards are **fail-closed**: an unauthenticated caller gets `401`, and an
|
|
43
|
+
upstream/PolyX failure gets `503` (distinct, never silently allowed).
|
|
44
|
+
|
|
45
|
+
## Exports
|
|
46
|
+
|
|
47
|
+
`createPolyX` · `requireUser` · `createExpressAdapter` · `introspectToken` ·
|
|
48
|
+
`IntrospectionResult`.
|
|
49
|
+
|
|
50
|
+
MIT licensed.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _poly_x_core = require("@poly-x/core");
|
|
3
|
+
//#region src/config.ts
|
|
4
|
+
function resolveServerConfig(options = {}) {
|
|
5
|
+
const secretKey = options.secretKey ?? process.env.POLYX_SECRET_KEY;
|
|
6
|
+
if (!secretKey) throw new Error("PolyX: missing secret key (set POLYX_SECRET_KEY or pass { secretKey }).");
|
|
7
|
+
const config = (0, _poly_x_core.resolveConfig)({
|
|
8
|
+
key: secretKey,
|
|
9
|
+
baseUrl: options.baseUrl,
|
|
10
|
+
context: "server"
|
|
11
|
+
});
|
|
12
|
+
if (config.kind !== "sk") throw new Error("PolyX: @poly-x/sdk needs a secret key (sk_…); a publishable key (pk_…) was provided.");
|
|
13
|
+
return {
|
|
14
|
+
config,
|
|
15
|
+
transport: options.transport ?? new _poly_x_core.FetchTransport(),
|
|
16
|
+
secretKey
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/endpoints.ts
|
|
21
|
+
/** Server-side PolyX endpoint paths (relative to the deployment base URL). */
|
|
22
|
+
const SERVER_ENDPOINTS = { introspect: "/v1/authz/introspect" };
|
|
23
|
+
/** Header carrying the App secret key — the v4 requireAppKey contract (ADR-0008). */
|
|
24
|
+
const SECRET_KEY_HEADER = "X-PolyX-Secret-Key";
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/introspect.ts
|
|
27
|
+
/**
|
|
28
|
+
* Token introspection (F008) — the server-side "is this caller's session
|
|
29
|
+
* valid, and who are they" check. Wraps poly-auth's `POST /v1/authz/introspect`
|
|
30
|
+
* and normalizes its authz body into a stable `IntrospectionResult`.
|
|
31
|
+
*/
|
|
32
|
+
function asRecord(value) {
|
|
33
|
+
return value && typeof value === "object" ? value : {};
|
|
34
|
+
}
|
|
35
|
+
function stringOrUndefined(value) {
|
|
36
|
+
return typeof value === "string" ? value : void 0;
|
|
37
|
+
}
|
|
38
|
+
function normalizeIntrospection(body) {
|
|
39
|
+
const b = asRecord(body);
|
|
40
|
+
if (!(b.active === true)) return {
|
|
41
|
+
active: false,
|
|
42
|
+
roles: [],
|
|
43
|
+
raw: body
|
|
44
|
+
};
|
|
45
|
+
const rolesRaw = b.organizationRoles;
|
|
46
|
+
return {
|
|
47
|
+
active: true,
|
|
48
|
+
userId: stringOrUndefined(b.userId),
|
|
49
|
+
organizationId: stringOrUndefined(b.organizationId) ?? stringOrUndefined(b.tenantId),
|
|
50
|
+
sessionId: stringOrUndefined(b.sessionId),
|
|
51
|
+
role: stringOrUndefined(b.role),
|
|
52
|
+
roles: Array.isArray(rolesRaw) ? rolesRaw.filter((r) => typeof r === "string") : [],
|
|
53
|
+
raw: body
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function introspectToken(deps, token) {
|
|
57
|
+
const response = await deps.transport.request({
|
|
58
|
+
method: "POST",
|
|
59
|
+
url: deps.config.baseUrl + SERVER_ENDPOINTS.introspect,
|
|
60
|
+
headers: { [SECRET_KEY_HEADER]: deps.secretKey },
|
|
61
|
+
body: { token }
|
|
62
|
+
});
|
|
63
|
+
if (response.status === 200) return normalizeIntrospection(response.body);
|
|
64
|
+
if (response.status === 401) return {
|
|
65
|
+
active: false,
|
|
66
|
+
roles: [],
|
|
67
|
+
raw: response.body
|
|
68
|
+
};
|
|
69
|
+
const error = (0, _poly_x_core.mapPlatformError)({
|
|
70
|
+
status: response.status,
|
|
71
|
+
body: response.body,
|
|
72
|
+
headers: response.headers
|
|
73
|
+
});
|
|
74
|
+
throw error instanceof _poly_x_core.UpstreamError ? error : new _poly_x_core.UpstreamError("PolyX introspection failed.", {
|
|
75
|
+
code: "INTROSPECT_FAILED",
|
|
76
|
+
meta: { status: response.status }
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/require-user.ts
|
|
81
|
+
/**
|
|
82
|
+
* Framework-agnostic request guard (F008). Extracts the bearer token, verifies
|
|
83
|
+
* it via introspection, and yields an authenticated context — fail-closed:
|
|
84
|
+
* missing/invalid token or an upstream failure both deny.
|
|
85
|
+
*/
|
|
86
|
+
function extractBearerToken(request) {
|
|
87
|
+
const raw = request.headers["authorization"] ?? request.headers["Authorization"];
|
|
88
|
+
const header = Array.isArray(raw) ? raw[0] : raw;
|
|
89
|
+
if (!header) return null;
|
|
90
|
+
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
91
|
+
return match ? match[1].trim() : null;
|
|
92
|
+
}
|
|
93
|
+
async function requireUser(deps, request) {
|
|
94
|
+
const token = extractBearerToken(request);
|
|
95
|
+
if (!token) throw new _poly_x_core.UnauthenticatedError("Missing bearer token.", {
|
|
96
|
+
code: "TOKEN_REQUIRED",
|
|
97
|
+
refreshable: false
|
|
98
|
+
});
|
|
99
|
+
const introspection = await introspectToken(deps, token);
|
|
100
|
+
if (!introspection.active || !introspection.userId) throw new _poly_x_core.UnauthenticatedError("Session is not active.", {
|
|
101
|
+
code: "UNAUTHENTICATED",
|
|
102
|
+
refreshable: false
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
userId: introspection.userId,
|
|
106
|
+
organizationId: introspection.organizationId,
|
|
107
|
+
sessionId: introspection.sessionId,
|
|
108
|
+
role: introspection.role,
|
|
109
|
+
roles: introspection.roles,
|
|
110
|
+
introspection
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/express.ts
|
|
115
|
+
/**
|
|
116
|
+
* Express/Connect adapter (F008), typed structurally so @poly-x/sdk needs no
|
|
117
|
+
* Express dependency. `requireUser()` attaches `req.polyUser` or fails closed
|
|
118
|
+
* with a 401.
|
|
119
|
+
*/
|
|
120
|
+
function createExpressAdapter(deps) {
|
|
121
|
+
return { requireUser() {
|
|
122
|
+
return async (req, res, next) => {
|
|
123
|
+
try {
|
|
124
|
+
req.polyUser = await requireUser(deps, req);
|
|
125
|
+
next();
|
|
126
|
+
} catch (error) {
|
|
127
|
+
const code = error instanceof _poly_x_core.PolyXError ? error.code : "UNAUTHENTICATED";
|
|
128
|
+
const status = error instanceof _poly_x_core.UpstreamError ? 503 : 401;
|
|
129
|
+
res.status(status).json({ error: code });
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
} };
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/client.ts
|
|
136
|
+
/**
|
|
137
|
+
* The unified server client (F008). v1 ships the auth slice: token
|
|
138
|
+
* introspection + request guards. Notifications/logs/api pillars namespace in
|
|
139
|
+
* here later (the Clerk-style unified shape).
|
|
140
|
+
*/
|
|
141
|
+
function createPolyX(options = {}) {
|
|
142
|
+
const resolved = resolveServerConfig(options);
|
|
143
|
+
const deps = {
|
|
144
|
+
config: resolved.config,
|
|
145
|
+
transport: resolved.transport,
|
|
146
|
+
secretKey: resolved.secretKey
|
|
147
|
+
};
|
|
148
|
+
return {
|
|
149
|
+
auth: { introspect: (token) => introspectToken(deps, token) },
|
|
150
|
+
express: createExpressAdapter(deps),
|
|
151
|
+
requireUser: (request) => requireUser(deps, request)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/index.ts
|
|
156
|
+
/**
|
|
157
|
+
* @poly-x/sdk — the server-side client for consuming Node backends (the `sk_`
|
|
158
|
+
* key side). v1 ships the auth slice: introspection + request guards.
|
|
159
|
+
*/
|
|
160
|
+
const PACKAGE_NAME = "@poly-x/sdk";
|
|
161
|
+
const version = "0.0.0";
|
|
162
|
+
//#endregion
|
|
163
|
+
exports.PACKAGE_NAME = PACKAGE_NAME;
|
|
164
|
+
exports.SECRET_KEY_HEADER = SECRET_KEY_HEADER;
|
|
165
|
+
exports.SERVER_ENDPOINTS = SERVER_ENDPOINTS;
|
|
166
|
+
exports.createExpressAdapter = createExpressAdapter;
|
|
167
|
+
exports.createPolyX = createPolyX;
|
|
168
|
+
exports.extractBearerToken = extractBearerToken;
|
|
169
|
+
exports.introspectToken = introspectToken;
|
|
170
|
+
exports.normalizeIntrospection = normalizeIntrospection;
|
|
171
|
+
exports.requireUser = requireUser;
|
|
172
|
+
exports.version = version;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { ResolvedConfig, Transport } from "@poly-x/core";
|
|
2
|
+
|
|
3
|
+
//#region src/config.d.ts
|
|
4
|
+
interface PolyXOptions {
|
|
5
|
+
/** The `sk_…` secret key. Default source: `POLYX_SECRET_KEY`. */
|
|
6
|
+
secretKey?: string;
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
/** Advanced: inject a transport (tests supply a mock). */
|
|
9
|
+
transport?: Transport;
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/introspect.d.ts
|
|
13
|
+
interface IntrospectionResult {
|
|
14
|
+
active: boolean;
|
|
15
|
+
userId?: string;
|
|
16
|
+
organizationId?: string;
|
|
17
|
+
sessionId?: string;
|
|
18
|
+
role?: string;
|
|
19
|
+
roles: readonly string[];
|
|
20
|
+
/** The raw platform body, for fields not surfaced here. */
|
|
21
|
+
raw: unknown;
|
|
22
|
+
}
|
|
23
|
+
declare function normalizeIntrospection(body: unknown): IntrospectionResult;
|
|
24
|
+
interface IntrospectDeps {
|
|
25
|
+
config: ResolvedConfig;
|
|
26
|
+
transport: Transport;
|
|
27
|
+
secretKey: string;
|
|
28
|
+
}
|
|
29
|
+
declare function introspectToken(deps: IntrospectDeps, token: string): Promise<IntrospectionResult>;
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/require-user.d.ts
|
|
32
|
+
interface AuthenticatedContext {
|
|
33
|
+
userId: string;
|
|
34
|
+
organizationId?: string;
|
|
35
|
+
sessionId?: string;
|
|
36
|
+
role?: string;
|
|
37
|
+
roles: readonly string[];
|
|
38
|
+
introspection: IntrospectionResult;
|
|
39
|
+
}
|
|
40
|
+
/** A minimal, structural request shape — compatible with Express/Fetch/Node. */
|
|
41
|
+
interface HeaderCarrier {
|
|
42
|
+
headers: Record<string, string | string[] | undefined>;
|
|
43
|
+
}
|
|
44
|
+
declare function extractBearerToken(request: HeaderCarrier): string | null;
|
|
45
|
+
declare function requireUser(deps: IntrospectDeps, request: HeaderCarrier): Promise<AuthenticatedContext>;
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/express.d.ts
|
|
48
|
+
interface ExpressLikeRequest {
|
|
49
|
+
headers: Record<string, string | string[] | undefined>;
|
|
50
|
+
polyUser?: AuthenticatedContext;
|
|
51
|
+
}
|
|
52
|
+
interface ExpressLikeResponse {
|
|
53
|
+
status(code: number): ExpressLikeResponse;
|
|
54
|
+
json(body: unknown): unknown;
|
|
55
|
+
}
|
|
56
|
+
type ExpressLikeNext = (error?: unknown) => void;
|
|
57
|
+
declare function createExpressAdapter(deps: IntrospectDeps): {
|
|
58
|
+
requireUser(): (req: ExpressLikeRequest, res: ExpressLikeResponse, next: ExpressLikeNext) => Promise<void>;
|
|
59
|
+
};
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/client.d.ts
|
|
62
|
+
interface PolyX {
|
|
63
|
+
auth: {
|
|
64
|
+
introspect(token: string): Promise<IntrospectionResult>;
|
|
65
|
+
};
|
|
66
|
+
express: ReturnType<typeof createExpressAdapter>;
|
|
67
|
+
/** Framework-agnostic guard for non-Express servers. */
|
|
68
|
+
requireUser(request: HeaderCarrier): Promise<AuthenticatedContext>;
|
|
69
|
+
}
|
|
70
|
+
declare function createPolyX(options?: PolyXOptions): PolyX;
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/endpoints.d.ts
|
|
73
|
+
/** Server-side PolyX endpoint paths (relative to the deployment base URL). */
|
|
74
|
+
declare const SERVER_ENDPOINTS: {
|
|
75
|
+
readonly introspect: "/v1/authz/introspect";
|
|
76
|
+
};
|
|
77
|
+
/** Header carrying the App secret key — the v4 requireAppKey contract (ADR-0008). */
|
|
78
|
+
declare const SECRET_KEY_HEADER = "X-PolyX-Secret-Key";
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/index.d.ts
|
|
81
|
+
/**
|
|
82
|
+
* @poly-x/sdk — the server-side client for consuming Node backends (the `sk_`
|
|
83
|
+
* key side). v1 ships the auth slice: introspection + request guards.
|
|
84
|
+
*/
|
|
85
|
+
declare const PACKAGE_NAME = "@poly-x/sdk";
|
|
86
|
+
declare const version = "0.0.0";
|
|
87
|
+
//#endregion
|
|
88
|
+
export { type AuthenticatedContext, type ExpressLikeNext, type ExpressLikeRequest, type ExpressLikeResponse, type HeaderCarrier, type IntrospectionResult, PACKAGE_NAME, type PolyX, type PolyXOptions, SECRET_KEY_HEADER, SERVER_ENDPOINTS, createExpressAdapter, createPolyX, extractBearerToken, introspectToken, normalizeIntrospection, requireUser, version };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { ResolvedConfig, Transport } from "@poly-x/core";
|
|
2
|
+
|
|
3
|
+
//#region src/config.d.ts
|
|
4
|
+
interface PolyXOptions {
|
|
5
|
+
/** The `sk_…` secret key. Default source: `POLYX_SECRET_KEY`. */
|
|
6
|
+
secretKey?: string;
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
/** Advanced: inject a transport (tests supply a mock). */
|
|
9
|
+
transport?: Transport;
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/introspect.d.ts
|
|
13
|
+
interface IntrospectionResult {
|
|
14
|
+
active: boolean;
|
|
15
|
+
userId?: string;
|
|
16
|
+
organizationId?: string;
|
|
17
|
+
sessionId?: string;
|
|
18
|
+
role?: string;
|
|
19
|
+
roles: readonly string[];
|
|
20
|
+
/** The raw platform body, for fields not surfaced here. */
|
|
21
|
+
raw: unknown;
|
|
22
|
+
}
|
|
23
|
+
declare function normalizeIntrospection(body: unknown): IntrospectionResult;
|
|
24
|
+
interface IntrospectDeps {
|
|
25
|
+
config: ResolvedConfig;
|
|
26
|
+
transport: Transport;
|
|
27
|
+
secretKey: string;
|
|
28
|
+
}
|
|
29
|
+
declare function introspectToken(deps: IntrospectDeps, token: string): Promise<IntrospectionResult>;
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/require-user.d.ts
|
|
32
|
+
interface AuthenticatedContext {
|
|
33
|
+
userId: string;
|
|
34
|
+
organizationId?: string;
|
|
35
|
+
sessionId?: string;
|
|
36
|
+
role?: string;
|
|
37
|
+
roles: readonly string[];
|
|
38
|
+
introspection: IntrospectionResult;
|
|
39
|
+
}
|
|
40
|
+
/** A minimal, structural request shape — compatible with Express/Fetch/Node. */
|
|
41
|
+
interface HeaderCarrier {
|
|
42
|
+
headers: Record<string, string | string[] | undefined>;
|
|
43
|
+
}
|
|
44
|
+
declare function extractBearerToken(request: HeaderCarrier): string | null;
|
|
45
|
+
declare function requireUser(deps: IntrospectDeps, request: HeaderCarrier): Promise<AuthenticatedContext>;
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/express.d.ts
|
|
48
|
+
interface ExpressLikeRequest {
|
|
49
|
+
headers: Record<string, string | string[] | undefined>;
|
|
50
|
+
polyUser?: AuthenticatedContext;
|
|
51
|
+
}
|
|
52
|
+
interface ExpressLikeResponse {
|
|
53
|
+
status(code: number): ExpressLikeResponse;
|
|
54
|
+
json(body: unknown): unknown;
|
|
55
|
+
}
|
|
56
|
+
type ExpressLikeNext = (error?: unknown) => void;
|
|
57
|
+
declare function createExpressAdapter(deps: IntrospectDeps): {
|
|
58
|
+
requireUser(): (req: ExpressLikeRequest, res: ExpressLikeResponse, next: ExpressLikeNext) => Promise<void>;
|
|
59
|
+
};
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/client.d.ts
|
|
62
|
+
interface PolyX {
|
|
63
|
+
auth: {
|
|
64
|
+
introspect(token: string): Promise<IntrospectionResult>;
|
|
65
|
+
};
|
|
66
|
+
express: ReturnType<typeof createExpressAdapter>;
|
|
67
|
+
/** Framework-agnostic guard for non-Express servers. */
|
|
68
|
+
requireUser(request: HeaderCarrier): Promise<AuthenticatedContext>;
|
|
69
|
+
}
|
|
70
|
+
declare function createPolyX(options?: PolyXOptions): PolyX;
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/endpoints.d.ts
|
|
73
|
+
/** Server-side PolyX endpoint paths (relative to the deployment base URL). */
|
|
74
|
+
declare const SERVER_ENDPOINTS: {
|
|
75
|
+
readonly introspect: "/v1/authz/introspect";
|
|
76
|
+
};
|
|
77
|
+
/** Header carrying the App secret key — the v4 requireAppKey contract (ADR-0008). */
|
|
78
|
+
declare const SECRET_KEY_HEADER = "X-PolyX-Secret-Key";
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/index.d.ts
|
|
81
|
+
/**
|
|
82
|
+
* @poly-x/sdk — the server-side client for consuming Node backends (the `sk_`
|
|
83
|
+
* key side). v1 ships the auth slice: introspection + request guards.
|
|
84
|
+
*/
|
|
85
|
+
declare const PACKAGE_NAME = "@poly-x/sdk";
|
|
86
|
+
declare const version = "0.0.0";
|
|
87
|
+
//#endregion
|
|
88
|
+
export { type AuthenticatedContext, type ExpressLikeNext, type ExpressLikeRequest, type ExpressLikeResponse, type HeaderCarrier, type IntrospectionResult, PACKAGE_NAME, type PolyX, type PolyXOptions, SECRET_KEY_HEADER, SERVER_ENDPOINTS, createExpressAdapter, createPolyX, extractBearerToken, introspectToken, normalizeIntrospection, requireUser, version };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { FetchTransport, PolyXError, UnauthenticatedError, UpstreamError, mapPlatformError, resolveConfig } from "@poly-x/core";
|
|
2
|
+
//#region src/config.ts
|
|
3
|
+
function resolveServerConfig(options = {}) {
|
|
4
|
+
const secretKey = options.secretKey ?? process.env.POLYX_SECRET_KEY;
|
|
5
|
+
if (!secretKey) throw new Error("PolyX: missing secret key (set POLYX_SECRET_KEY or pass { secretKey }).");
|
|
6
|
+
const config = resolveConfig({
|
|
7
|
+
key: secretKey,
|
|
8
|
+
baseUrl: options.baseUrl,
|
|
9
|
+
context: "server"
|
|
10
|
+
});
|
|
11
|
+
if (config.kind !== "sk") throw new Error("PolyX: @poly-x/sdk needs a secret key (sk_…); a publishable key (pk_…) was provided.");
|
|
12
|
+
return {
|
|
13
|
+
config,
|
|
14
|
+
transport: options.transport ?? new FetchTransport(),
|
|
15
|
+
secretKey
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/endpoints.ts
|
|
20
|
+
/** Server-side PolyX endpoint paths (relative to the deployment base URL). */
|
|
21
|
+
const SERVER_ENDPOINTS = { introspect: "/v1/authz/introspect" };
|
|
22
|
+
/** Header carrying the App secret key — the v4 requireAppKey contract (ADR-0008). */
|
|
23
|
+
const SECRET_KEY_HEADER = "X-PolyX-Secret-Key";
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/introspect.ts
|
|
26
|
+
/**
|
|
27
|
+
* Token introspection (F008) — the server-side "is this caller's session
|
|
28
|
+
* valid, and who are they" check. Wraps poly-auth's `POST /v1/authz/introspect`
|
|
29
|
+
* and normalizes its authz body into a stable `IntrospectionResult`.
|
|
30
|
+
*/
|
|
31
|
+
function asRecord(value) {
|
|
32
|
+
return value && typeof value === "object" ? value : {};
|
|
33
|
+
}
|
|
34
|
+
function stringOrUndefined(value) {
|
|
35
|
+
return typeof value === "string" ? value : void 0;
|
|
36
|
+
}
|
|
37
|
+
function normalizeIntrospection(body) {
|
|
38
|
+
const b = asRecord(body);
|
|
39
|
+
if (!(b.active === true)) return {
|
|
40
|
+
active: false,
|
|
41
|
+
roles: [],
|
|
42
|
+
raw: body
|
|
43
|
+
};
|
|
44
|
+
const rolesRaw = b.organizationRoles;
|
|
45
|
+
return {
|
|
46
|
+
active: true,
|
|
47
|
+
userId: stringOrUndefined(b.userId),
|
|
48
|
+
organizationId: stringOrUndefined(b.organizationId) ?? stringOrUndefined(b.tenantId),
|
|
49
|
+
sessionId: stringOrUndefined(b.sessionId),
|
|
50
|
+
role: stringOrUndefined(b.role),
|
|
51
|
+
roles: Array.isArray(rolesRaw) ? rolesRaw.filter((r) => typeof r === "string") : [],
|
|
52
|
+
raw: body
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function introspectToken(deps, token) {
|
|
56
|
+
const response = await deps.transport.request({
|
|
57
|
+
method: "POST",
|
|
58
|
+
url: deps.config.baseUrl + SERVER_ENDPOINTS.introspect,
|
|
59
|
+
headers: { [SECRET_KEY_HEADER]: deps.secretKey },
|
|
60
|
+
body: { token }
|
|
61
|
+
});
|
|
62
|
+
if (response.status === 200) return normalizeIntrospection(response.body);
|
|
63
|
+
if (response.status === 401) return {
|
|
64
|
+
active: false,
|
|
65
|
+
roles: [],
|
|
66
|
+
raw: response.body
|
|
67
|
+
};
|
|
68
|
+
const error = mapPlatformError({
|
|
69
|
+
status: response.status,
|
|
70
|
+
body: response.body,
|
|
71
|
+
headers: response.headers
|
|
72
|
+
});
|
|
73
|
+
throw error instanceof UpstreamError ? error : new UpstreamError("PolyX introspection failed.", {
|
|
74
|
+
code: "INTROSPECT_FAILED",
|
|
75
|
+
meta: { status: response.status }
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/require-user.ts
|
|
80
|
+
/**
|
|
81
|
+
* Framework-agnostic request guard (F008). Extracts the bearer token, verifies
|
|
82
|
+
* it via introspection, and yields an authenticated context — fail-closed:
|
|
83
|
+
* missing/invalid token or an upstream failure both deny.
|
|
84
|
+
*/
|
|
85
|
+
function extractBearerToken(request) {
|
|
86
|
+
const raw = request.headers["authorization"] ?? request.headers["Authorization"];
|
|
87
|
+
const header = Array.isArray(raw) ? raw[0] : raw;
|
|
88
|
+
if (!header) return null;
|
|
89
|
+
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
90
|
+
return match ? match[1].trim() : null;
|
|
91
|
+
}
|
|
92
|
+
async function requireUser(deps, request) {
|
|
93
|
+
const token = extractBearerToken(request);
|
|
94
|
+
if (!token) throw new UnauthenticatedError("Missing bearer token.", {
|
|
95
|
+
code: "TOKEN_REQUIRED",
|
|
96
|
+
refreshable: false
|
|
97
|
+
});
|
|
98
|
+
const introspection = await introspectToken(deps, token);
|
|
99
|
+
if (!introspection.active || !introspection.userId) throw new UnauthenticatedError("Session is not active.", {
|
|
100
|
+
code: "UNAUTHENTICATED",
|
|
101
|
+
refreshable: false
|
|
102
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
userId: introspection.userId,
|
|
105
|
+
organizationId: introspection.organizationId,
|
|
106
|
+
sessionId: introspection.sessionId,
|
|
107
|
+
role: introspection.role,
|
|
108
|
+
roles: introspection.roles,
|
|
109
|
+
introspection
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/express.ts
|
|
114
|
+
/**
|
|
115
|
+
* Express/Connect adapter (F008), typed structurally so @poly-x/sdk needs no
|
|
116
|
+
* Express dependency. `requireUser()` attaches `req.polyUser` or fails closed
|
|
117
|
+
* with a 401.
|
|
118
|
+
*/
|
|
119
|
+
function createExpressAdapter(deps) {
|
|
120
|
+
return { requireUser() {
|
|
121
|
+
return async (req, res, next) => {
|
|
122
|
+
try {
|
|
123
|
+
req.polyUser = await requireUser(deps, req);
|
|
124
|
+
next();
|
|
125
|
+
} catch (error) {
|
|
126
|
+
const code = error instanceof PolyXError ? error.code : "UNAUTHENTICATED";
|
|
127
|
+
const status = error instanceof UpstreamError ? 503 : 401;
|
|
128
|
+
res.status(status).json({ error: code });
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
} };
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/client.ts
|
|
135
|
+
/**
|
|
136
|
+
* The unified server client (F008). v1 ships the auth slice: token
|
|
137
|
+
* introspection + request guards. Notifications/logs/api pillars namespace in
|
|
138
|
+
* here later (the Clerk-style unified shape).
|
|
139
|
+
*/
|
|
140
|
+
function createPolyX(options = {}) {
|
|
141
|
+
const resolved = resolveServerConfig(options);
|
|
142
|
+
const deps = {
|
|
143
|
+
config: resolved.config,
|
|
144
|
+
transport: resolved.transport,
|
|
145
|
+
secretKey: resolved.secretKey
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
auth: { introspect: (token) => introspectToken(deps, token) },
|
|
149
|
+
express: createExpressAdapter(deps),
|
|
150
|
+
requireUser: (request) => requireUser(deps, request)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/index.ts
|
|
155
|
+
/**
|
|
156
|
+
* @poly-x/sdk — the server-side client for consuming Node backends (the `sk_`
|
|
157
|
+
* key side). v1 ships the auth slice: introspection + request guards.
|
|
158
|
+
*/
|
|
159
|
+
const PACKAGE_NAME = "@poly-x/sdk";
|
|
160
|
+
const version = "0.0.0";
|
|
161
|
+
//#endregion
|
|
162
|
+
export { PACKAGE_NAME, SECRET_KEY_HEADER, SERVER_ENDPOINTS, createExpressAdapter, createPolyX, extractBearerToken, introspectToken, normalizeIntrospection, requireUser, version };
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@poly-x/sdk",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "PolyX SDK for Node servers - unified client and framework adapters",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": {
|
|
14
|
+
"types": "./dist/index.d.mts",
|
|
15
|
+
"default": "./dist/index.mjs"
|
|
16
|
+
},
|
|
17
|
+
"require": {
|
|
18
|
+
"types": "./dist/index.d.cts",
|
|
19
|
+
"default": "./dist/index.cjs"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"main": "./dist/index.cjs",
|
|
24
|
+
"module": "./dist/index.mjs",
|
|
25
|
+
"types": "./dist/index.d.cts",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@poly-x/core": "0.1.0-alpha.0"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://dev.azure.com/waiindustries/wAI%20Engineering/_git/poly-x-sdk"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=20"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsdown",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"lint": "eslint src"
|
|
43
|
+
}
|
|
44
|
+
}
|