@godxjp/server-sdk 0.1.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 +201 -0
- package/README.md +486 -0
- package/dist/auth.d.ts +37 -0
- package/dist/auth.js +16 -0
- package/dist/auth.js.map +1 -0
- package/dist/authorization.d.ts +16 -0
- package/dist/authorization.js +31 -0
- package/dist/authorization.js.map +1 -0
- package/dist/browser-guard.d.ts +2 -0
- package/dist/browser-guard.js +5 -0
- package/dist/browser-guard.js.map +1 -0
- package/dist/chunk-DMXYEWYY.js +132 -0
- package/dist/chunk-DMXYEWYY.js.map +1 -0
- package/dist/chunk-GPGSVZCR.js +87 -0
- package/dist/chunk-GPGSVZCR.js.map +1 -0
- package/dist/hono.d.ts +35 -0
- package/dist/hono.js +159 -0
- package/dist/hono.js.map +1 -0
- package/dist/http.d.ts +96 -0
- package/dist/http.js +13 -0
- package/dist/http.js.map +1 -0
- package/dist/org.d.ts +126 -0
- package/dist/org.js +101 -0
- package/dist/org.js.map +1 -0
- package/dist/sync.d.ts +174 -0
- package/dist/sync.js +241 -0
- package/dist/sync.js.map +1 -0
- package/package.json +89 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// src/http.ts
|
|
2
|
+
import { createRemoteJWKSet } from "jose";
|
|
3
|
+
var GodxHttpError = class extends Error {
|
|
4
|
+
status;
|
|
5
|
+
body;
|
|
6
|
+
constructor(status, body, message) {
|
|
7
|
+
super(message ?? `GoDX platform request failed with ${status}`);
|
|
8
|
+
this.name = "GodxHttpError";
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.body = body;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var issuerCache = /* @__PURE__ */ new Map();
|
|
14
|
+
function loadIssuer(issuerUrl) {
|
|
15
|
+
let cached = issuerCache.get(issuerUrl);
|
|
16
|
+
if (!cached) {
|
|
17
|
+
cached = (async () => {
|
|
18
|
+
const res = await fetch(new URL("/.well-known/openid-configuration", issuerUrl));
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
issuerCache.delete(issuerUrl);
|
|
21
|
+
throw new GodxHttpError(res.status, await res.text(), "OIDC discovery failed");
|
|
22
|
+
}
|
|
23
|
+
const metadata = await res.json();
|
|
24
|
+
return { metadata, jwks: createRemoteJWKSet(new URL(metadata.jwks_uri)) };
|
|
25
|
+
})();
|
|
26
|
+
issuerCache.set(issuerUrl, cached);
|
|
27
|
+
cached.catch(() => issuerCache.delete(issuerUrl));
|
|
28
|
+
}
|
|
29
|
+
return cached;
|
|
30
|
+
}
|
|
31
|
+
var GodxSsoClient = class {
|
|
32
|
+
config;
|
|
33
|
+
constructor(config) {
|
|
34
|
+
this.config = config;
|
|
35
|
+
}
|
|
36
|
+
async authorizeUrl(params) {
|
|
37
|
+
const { metadata } = await loadIssuer(this.config.issuer);
|
|
38
|
+
const url = new URL(metadata.authorization_endpoint);
|
|
39
|
+
url.search = new URLSearchParams({
|
|
40
|
+
service_slug: this.config.serviceSlug,
|
|
41
|
+
client_id: this.config.clientId,
|
|
42
|
+
redirect_uri: this.config.redirectUri,
|
|
43
|
+
response_type: "code",
|
|
44
|
+
scope: params.scope ?? "openid profile email",
|
|
45
|
+
state: params.state,
|
|
46
|
+
nonce: params.nonce,
|
|
47
|
+
code_challenge: params.codeChallenge,
|
|
48
|
+
code_challenge_method: "S256",
|
|
49
|
+
...params.organizationContextId ? { organization_context_id: params.organizationContextId } : {}
|
|
50
|
+
}).toString();
|
|
51
|
+
return url.toString();
|
|
52
|
+
}
|
|
53
|
+
async exchangeCode(params) {
|
|
54
|
+
return this.tokenRequest("token_endpoint", {
|
|
55
|
+
grant_type: "authorization_code",
|
|
56
|
+
redirect_uri: this.config.redirectUri,
|
|
57
|
+
code: params.code,
|
|
58
|
+
code_verifier: params.codeVerifier
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async refresh(refreshToken) {
|
|
62
|
+
const { metadata } = await loadIssuer(this.config.issuer);
|
|
63
|
+
return this.postForm(new URL("/api/sso/refresh", metadata.issuer).toString(), {
|
|
64
|
+
grant_type: "refresh_token",
|
|
65
|
+
refresh_token: refreshToken
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** Best-effort revocation; failures are the caller's decision to ignore. */
|
|
69
|
+
async revoke(token) {
|
|
70
|
+
const { metadata } = await loadIssuer(this.config.issuer);
|
|
71
|
+
if (!metadata.revocation_endpoint) return;
|
|
72
|
+
await this.postForm(metadata.revocation_endpoint, { token });
|
|
73
|
+
}
|
|
74
|
+
async userinfo(accessToken) {
|
|
75
|
+
const { metadata } = await loadIssuer(this.config.issuer);
|
|
76
|
+
const res = await fetch(metadata.userinfo_endpoint, {
|
|
77
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
78
|
+
});
|
|
79
|
+
if (!res.ok) throw new GodxHttpError(res.status, await res.text(), "userinfo failed");
|
|
80
|
+
return await res.json();
|
|
81
|
+
}
|
|
82
|
+
async tokenRequest(endpoint, fields) {
|
|
83
|
+
const { metadata } = await loadIssuer(this.config.issuer);
|
|
84
|
+
return this.postForm(metadata[endpoint], fields);
|
|
85
|
+
}
|
|
86
|
+
async postForm(url, fields) {
|
|
87
|
+
const body = new URLSearchParams({
|
|
88
|
+
service_slug: this.config.serviceSlug,
|
|
89
|
+
client_id: this.config.clientId,
|
|
90
|
+
...this.config.clientSecret ? { client_secret: this.config.clientSecret } : {},
|
|
91
|
+
...fields
|
|
92
|
+
});
|
|
93
|
+
const res = await fetch(url, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
96
|
+
body
|
|
97
|
+
});
|
|
98
|
+
if (!res.ok) throw new GodxHttpError(res.status, await res.text(), "token request failed");
|
|
99
|
+
return await res.json();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
function createServiceApi(config) {
|
|
103
|
+
const get = async (path, query) => {
|
|
104
|
+
const url = new URL(path, config.issuer);
|
|
105
|
+
url.search = new URLSearchParams(query).toString();
|
|
106
|
+
const res = await fetch(url, {
|
|
107
|
+
headers: {
|
|
108
|
+
"X-Service-Slug": config.serviceSlug,
|
|
109
|
+
"X-Service-Secret": config.serviceSecret
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
if (!res.ok) throw new GodxHttpError(res.status, await res.text(), `service API ${path} failed`);
|
|
113
|
+
return await res.json();
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
users: (q) => get("/api/sso/service/users", {
|
|
117
|
+
organization_slug: q.organizationSlug,
|
|
118
|
+
...q.perPage ? { per_page: String(q.perPage) } : {},
|
|
119
|
+
...q.page ? { page: String(q.page) } : {}
|
|
120
|
+
}),
|
|
121
|
+
branches: (q) => get("/api/sso/service/branches", { organization_slug: q.organizationSlug }),
|
|
122
|
+
brands: (q) => get("/api/sso/service/brands", { organization_slug: q.organizationSlug })
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export {
|
|
127
|
+
GodxHttpError,
|
|
128
|
+
loadIssuer,
|
|
129
|
+
GodxSsoClient,
|
|
130
|
+
createServiceApi
|
|
131
|
+
};
|
|
132
|
+
//# sourceMappingURL=chunk-DMXYEWYY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/http.ts"],"sourcesContent":["/**\n * ./http — endpoint contract from OIDC discovery, the SSO client, and the\n * server-to-server service API. Runtime-agnostic: fetch + WebCrypto only.\n * Mirrors php-sdk's Http module boundary.\n */\nimport { createRemoteJWKSet } from \"jose\";\n\nexport interface IssuerMetadata {\n issuer: string;\n authorization_endpoint: string;\n token_endpoint: string;\n userinfo_endpoint: string;\n jwks_uri: string;\n end_session_endpoint?: string;\n revocation_endpoint?: string;\n}\n\nexport interface Issuer {\n metadata: IssuerMetadata;\n jwks: ReturnType<typeof createRemoteJWKSet>;\n}\n\nexport class GodxHttpError extends Error {\n readonly status: number;\n readonly body: string;\n\n constructor(status: number, body: string, message?: string) {\n super(message ?? `GoDX platform request failed with ${status}`);\n this.name = \"GodxHttpError\";\n this.status = status;\n this.body = body;\n }\n}\n\n// Per-isolate cache; the platform docs require reading discovery, not hardcoding paths.\nconst issuerCache = new Map<string, Promise<Issuer>>();\n\nexport function loadIssuer(issuerUrl: string): Promise<Issuer> {\n let cached = issuerCache.get(issuerUrl);\n if (!cached) {\n cached = (async () => {\n const res = await fetch(new URL(\"/.well-known/openid-configuration\", issuerUrl));\n if (!res.ok) {\n issuerCache.delete(issuerUrl);\n throw new GodxHttpError(res.status, await res.text(), \"OIDC discovery failed\");\n }\n const metadata = (await res.json()) as IssuerMetadata;\n return { metadata, jwks: createRemoteJWKSet(new URL(metadata.jwks_uri)) };\n })();\n issuerCache.set(issuerUrl, cached);\n cached.catch(() => issuerCache.delete(issuerUrl));\n }\n return cached;\n}\n\nexport interface SsoClientConfig {\n issuer: string;\n serviceSlug: string;\n clientId: string;\n /** Omit for a public (secretless) instance — PKCE alone then. */\n clientSecret?: string;\n redirectUri: string;\n}\n\nexport interface TokenResponse {\n access_token: string;\n token_type: string;\n expires_in: number;\n id_token?: string;\n refresh_token?: string;\n scope?: string;\n}\n\nexport interface UserinfoResponse {\n sub: string;\n email?: string;\n email_verified?: boolean;\n name?: string;\n locale?: string;\n timezone?: string;\n}\n\nexport class GodxSsoClient {\n readonly config: SsoClientConfig;\n\n constructor(config: SsoClientConfig) {\n this.config = config;\n }\n\n async authorizeUrl(params: {\n state: string;\n nonce: string;\n codeChallenge: string;\n scope?: string;\n /** console_organization_id — required by the launch gates; usually arrives on the launch URL. */\n organizationContextId?: string;\n }): Promise<string> {\n const { metadata } = await loadIssuer(this.config.issuer);\n const url = new URL(metadata.authorization_endpoint);\n url.search = new URLSearchParams({\n service_slug: this.config.serviceSlug,\n client_id: this.config.clientId,\n redirect_uri: this.config.redirectUri,\n response_type: \"code\",\n scope: params.scope ?? \"openid profile email\",\n state: params.state,\n nonce: params.nonce,\n code_challenge: params.codeChallenge,\n code_challenge_method: \"S256\",\n ...(params.organizationContextId\n ? { organization_context_id: params.organizationContextId }\n : {}),\n }).toString();\n return url.toString();\n }\n\n async exchangeCode(params: { code: string; codeVerifier: string }): Promise<TokenResponse> {\n return this.tokenRequest(\"token_endpoint\", {\n grant_type: \"authorization_code\",\n redirect_uri: this.config.redirectUri,\n code: params.code,\n code_verifier: params.codeVerifier,\n });\n }\n\n async refresh(refreshToken: string): Promise<TokenResponse> {\n const { metadata } = await loadIssuer(this.config.issuer);\n return this.postForm(new URL(\"/api/sso/refresh\", metadata.issuer).toString(), {\n grant_type: \"refresh_token\",\n refresh_token: refreshToken,\n });\n }\n\n /** Best-effort revocation; failures are the caller's decision to ignore. */\n async revoke(token: string): Promise<void> {\n const { metadata } = await loadIssuer(this.config.issuer);\n if (!metadata.revocation_endpoint) return;\n await this.postForm(metadata.revocation_endpoint, { token });\n }\n\n async userinfo(accessToken: string): Promise<UserinfoResponse> {\n const { metadata } = await loadIssuer(this.config.issuer);\n const res = await fetch(metadata.userinfo_endpoint, {\n headers: { Authorization: `Bearer ${accessToken}` },\n });\n if (!res.ok) throw new GodxHttpError(res.status, await res.text(), \"userinfo failed\");\n return (await res.json()) as UserinfoResponse;\n }\n\n private async tokenRequest(\n endpoint: \"token_endpoint\",\n fields: Record<string, string>,\n ): Promise<TokenResponse> {\n const { metadata } = await loadIssuer(this.config.issuer);\n return this.postForm(metadata[endpoint], fields);\n }\n\n private async postForm(url: string, fields: Record<string, string>): Promise<TokenResponse> {\n const body = new URLSearchParams({\n service_slug: this.config.serviceSlug,\n client_id: this.config.clientId,\n ...(this.config.clientSecret ? { client_secret: this.config.clientSecret } : {}),\n ...fields,\n });\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body,\n });\n if (!res.ok) throw new GodxHttpError(res.status, await res.text(), \"token request failed\");\n return (await res.json()) as TokenResponse;\n }\n}\n\n/**\n * Server-to-server reads with service credentials (X-Service-Slug/X-Service-Secret).\n * Rate-limited by the platform (429) — cache results and back off; no auto-retry here.\n */\nexport function createServiceApi(config: {\n issuer: string;\n serviceSlug: string;\n serviceSecret: string;\n}) {\n const get = async <T>(path: string, query: Record<string, string>): Promise<T> => {\n const url = new URL(path, config.issuer);\n url.search = new URLSearchParams(query).toString();\n const res = await fetch(url, {\n headers: {\n \"X-Service-Slug\": config.serviceSlug,\n \"X-Service-Secret\": config.serviceSecret,\n },\n });\n if (!res.ok) throw new GodxHttpError(res.status, await res.text(), `service API ${path} failed`);\n return (await res.json()) as T;\n };\n\n return {\n users: (q: { organizationSlug: string; perPage?: number; page?: number }) =>\n get<unknown>(\"/api/sso/service/users\", {\n organization_slug: q.organizationSlug,\n ...(q.perPage ? { per_page: String(q.perPage) } : {}),\n ...(q.page ? { page: String(q.page) } : {}),\n }),\n branches: (q: { organizationSlug: string }) =>\n get<unknown>(\"/api/sso/service/branches\", { organization_slug: q.organizationSlug }),\n brands: (q: { organizationSlug: string }) =>\n get<unknown>(\"/api/sso/service/brands\", { organization_slug: q.organizationSlug }),\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadIssuer
|
|
3
|
+
} from "./chunk-DMXYEWYY.js";
|
|
4
|
+
|
|
5
|
+
// src/auth.ts
|
|
6
|
+
import { EncryptJWT, jwtDecrypt, jwtVerify } from "jose";
|
|
7
|
+
async function verifyAccessToken(token, opts) {
|
|
8
|
+
const expectedAudience = opts.audience ?? opts.serviceSlug;
|
|
9
|
+
const { metadata, jwks } = await loadIssuer(opts.issuer);
|
|
10
|
+
let payload;
|
|
11
|
+
try {
|
|
12
|
+
({ payload } = await jwtVerify(token, jwks, {
|
|
13
|
+
issuer: metadata.issuer,
|
|
14
|
+
audience: expectedAudience
|
|
15
|
+
}));
|
|
16
|
+
} catch (e) {
|
|
17
|
+
const actual = decodeUnverified(token);
|
|
18
|
+
throw new Error(
|
|
19
|
+
`${e instanceof Error ? e.message : String(e)} \u2014 expected iss="${metadata.issuer}" aud="${expectedAudience}", token carries iss="${String(actual?.iss ?? "?")}" aud=${JSON.stringify(actual?.aud ?? "?")}`,
|
|
20
|
+
{ cause: e }
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
const organizationId = payload["organization_id"];
|
|
24
|
+
if (typeof organizationId !== "string" || organizationId === "") {
|
|
25
|
+
throw new Error("access token has no organization_id claim");
|
|
26
|
+
}
|
|
27
|
+
if (typeof payload.sub !== "string" || typeof payload.exp !== "number") {
|
|
28
|
+
throw new Error("access token is missing sub/exp");
|
|
29
|
+
}
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
sub: payload.sub,
|
|
32
|
+
organizationId,
|
|
33
|
+
accessMode: payload["organization_access_mode"] === "full" ? "full" : "read_only",
|
|
34
|
+
scopes: Object.freeze(String(payload["scope"] ?? "").split(" ").filter(Boolean)),
|
|
35
|
+
serviceInstanceId: typeof payload["service_instance_id"] === "string" ? payload["service_instance_id"] : void 0,
|
|
36
|
+
clientId: typeof payload["client_id"] === "string" ? payload["client_id"] : void 0,
|
|
37
|
+
expiresAt: payload.exp
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async function createPkce() {
|
|
41
|
+
const verifier = randomToken(32);
|
|
42
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
43
|
+
return { verifier, challenge: base64url(new Uint8Array(digest)), method: "S256" };
|
|
44
|
+
}
|
|
45
|
+
function randomToken(bytes = 16) {
|
|
46
|
+
const buf = new Uint8Array(bytes);
|
|
47
|
+
crypto.getRandomValues(buf);
|
|
48
|
+
return base64url(buf);
|
|
49
|
+
}
|
|
50
|
+
function decodeUnverified(token) {
|
|
51
|
+
try {
|
|
52
|
+
const part = token.split(".")[1] ?? "";
|
|
53
|
+
const pad = part.replace(/-/g, "+").replace(/_/g, "/");
|
|
54
|
+
return JSON.parse(atob(pad));
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function base64url(bytes) {
|
|
60
|
+
let bin = "";
|
|
61
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
62
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
63
|
+
}
|
|
64
|
+
async function sessionKey(secret) {
|
|
65
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret));
|
|
66
|
+
return new Uint8Array(digest);
|
|
67
|
+
}
|
|
68
|
+
async function sealData(payload, secret, ttlSeconds) {
|
|
69
|
+
return new EncryptJWT(payload).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(Math.floor(Date.now() / 1e3) + ttlSeconds).encrypt(await sessionKey(secret));
|
|
70
|
+
}
|
|
71
|
+
async function openData(token, secret) {
|
|
72
|
+
try {
|
|
73
|
+
const { payload } = await jwtDecrypt(token, await sessionKey(secret));
|
|
74
|
+
return payload;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export {
|
|
81
|
+
verifyAccessToken,
|
|
82
|
+
createPkce,
|
|
83
|
+
randomToken,
|
|
84
|
+
sealData,
|
|
85
|
+
openData
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=chunk-GPGSVZCR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/auth.ts"],"sourcesContent":["/**\n * ./auth — verified immutable Principal, PKCE, and sealed session values.\n * Mirrors php-sdk's Auth module: the Principal only comes out of one parse\n * gate (verifyAccessToken) and is frozen after that.\n */\nimport { EncryptJWT, jwtDecrypt, jwtVerify } from \"jose\";\nimport { loadIssuer } from \"./http\";\n\nexport interface Principal {\n readonly sub: string;\n readonly organizationId: string;\n /** Defaults to read_only unless the claim says full — fail closed. */\n readonly accessMode: \"full\" | \"read_only\";\n readonly scopes: readonly string[];\n readonly serviceInstanceId?: string;\n readonly clientId?: string;\n /** Unix seconds, from the access token's exp. */\n readonly expiresAt: number;\n /** From userinfo, when the adapter fetched it at login. */\n readonly name?: string;\n readonly email?: string;\n}\n\n/**\n * The one parse gate. Validates signature (JWKS), iss (discovery issuer,\n * exact), aud (= service slug), exp — then requires organization_id.\n */\nexport async function verifyAccessToken(\n token: string,\n opts: {\n issuer: string;\n serviceSlug: string;\n /** The token audience = your ServiceInstance slug. Defaults to serviceSlug. */\n audience?: string;\n },\n): Promise<Principal> {\n const expectedAudience = opts.audience ?? opts.serviceSlug;\n const { metadata, jwks } = await loadIssuer(opts.issuer);\n let payload;\n try {\n ({ payload } = await jwtVerify(token, jwks, {\n issuer: metadata.issuer,\n audience: expectedAudience,\n }));\n } catch (e) {\n // Say what was expected vs what the token actually carries — a bare\n // \"unexpected claim\" costs whoever reads it a debugging session.\n const actual = decodeUnverified(token);\n throw new Error(\n `${e instanceof Error ? e.message : String(e)} — expected iss=\"${metadata.issuer}\" aud=\"${expectedAudience}\", ` +\n `token carries iss=\"${String(actual?.iss ?? \"?\")}\" aud=${JSON.stringify(actual?.aud ?? \"?\")}`,\n { cause: e },\n );\n }\n\n const organizationId = payload[\"organization_id\"];\n if (typeof organizationId !== \"string\" || organizationId === \"\") {\n throw new Error(\"access token has no organization_id claim\");\n }\n if (typeof payload.sub !== \"string\" || typeof payload.exp !== \"number\") {\n throw new Error(\"access token is missing sub/exp\");\n }\n\n return Object.freeze({\n sub: payload.sub,\n organizationId,\n accessMode: payload[\"organization_access_mode\"] === \"full\" ? \"full\" : \"read_only\",\n scopes: Object.freeze(String(payload[\"scope\"] ?? \"\").split(\" \").filter(Boolean)),\n serviceInstanceId: typeof payload[\"service_instance_id\"] === \"string\" ? payload[\"service_instance_id\"] : undefined,\n clientId: typeof payload[\"client_id\"] === \"string\" ? payload[\"client_id\"] : undefined,\n expiresAt: payload.exp,\n } satisfies Principal);\n}\n\nexport interface Pkce {\n verifier: string;\n challenge: string;\n method: \"S256\";\n}\n\nexport async function createPkce(): Promise<Pkce> {\n const verifier = randomToken(32);\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(verifier));\n return { verifier, challenge: base64url(new Uint8Array(digest)), method: \"S256\" };\n}\n\nexport function randomToken(bytes = 16): string {\n const buf = new Uint8Array(bytes);\n crypto.getRandomValues(buf);\n return base64url(buf);\n}\n\n/** Diagnostics only — an UNVERIFIED read of the payload for error messages. */\nfunction decodeUnverified(token: string): Record<string, unknown> | null {\n try {\n const part = token.split(\".\")[1] ?? \"\";\n const pad = part.replace(/-/g, \"+\").replace(/_/g, \"/\");\n return JSON.parse(atob(pad)) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\nfunction base64url(bytes: Uint8Array): string {\n let bin = \"\";\n for (const b of bytes) bin += String.fromCharCode(b);\n return btoa(bin).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nasync function sessionKey(secret: string): Promise<Uint8Array> {\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(secret));\n return new Uint8Array(digest);\n}\n\n/** Encrypt + sign an arbitrary payload into a compact cookie value (JWE dir/A256GCM). */\nexport async function sealData(\n payload: Record<string, unknown>,\n secret: string,\n ttlSeconds: number,\n): Promise<string> {\n return new EncryptJWT(payload)\n .setProtectedHeader({ alg: \"dir\", enc: \"A256GCM\" })\n .setIssuedAt()\n .setExpirationTime(Math.floor(Date.now() / 1000) + ttlSeconds)\n .encrypt(await sessionKey(secret));\n}\n\n/** Returns null for anything invalid or expired — never throws on bad input. */\nexport async function openData<T = Record<string, unknown>>(\n token: string,\n secret: string,\n): Promise<T | null> {\n try {\n const { payload } = await jwtDecrypt(token, await sessionKey(secret));\n return payload as T;\n } catch {\n return null;\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/hono.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as hono_types from 'hono/types';
|
|
2
|
+
import { Env, Hono, MiddlewareHandler } from 'hono';
|
|
3
|
+
import { Principal } from './auth.js';
|
|
4
|
+
|
|
5
|
+
interface GodxSsoConfig {
|
|
6
|
+
issuer: string;
|
|
7
|
+
serviceSlug: string;
|
|
8
|
+
clientId: string;
|
|
9
|
+
clientSecret?: string;
|
|
10
|
+
redirectUri: string;
|
|
11
|
+
sessionSecret: string;
|
|
12
|
+
scope?: string;
|
|
13
|
+
/** Token audience = your ServiceInstance slug (defaults to serviceSlug). */
|
|
14
|
+
audience?: string;
|
|
15
|
+
/**
|
|
16
|
+
* console_organization_id to fall back on when the login was not started
|
|
17
|
+
* from a platform launch URL (which carries organization_context_id).
|
|
18
|
+
* Useful in local dev; leave unset in production multi-org setups.
|
|
19
|
+
*/
|
|
20
|
+
defaultOrganizationContextId?: string;
|
|
21
|
+
}
|
|
22
|
+
type GodxVariables = {
|
|
23
|
+
principal: Principal;
|
|
24
|
+
accessToken: string;
|
|
25
|
+
};
|
|
26
|
+
declare function godxSso<E extends Env & {
|
|
27
|
+
Variables: GodxVariables;
|
|
28
|
+
}>(getConfig: (env: E["Bindings"]) => GodxSsoConfig): {
|
|
29
|
+
routes: Hono<E, hono_types.BlankSchema, "/">;
|
|
30
|
+
requireAuth: MiddlewareHandler<E>;
|
|
31
|
+
requireWritable: MiddlewareHandler<E>;
|
|
32
|
+
dockProxy: MiddlewareHandler<E>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export { type GodxSsoConfig, type GodxVariables, godxSso };
|
package/dist/hono.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createPkce,
|
|
3
|
+
openData,
|
|
4
|
+
randomToken,
|
|
5
|
+
sealData,
|
|
6
|
+
verifyAccessToken
|
|
7
|
+
} from "./chunk-GPGSVZCR.js";
|
|
8
|
+
import {
|
|
9
|
+
GodxSsoClient,
|
|
10
|
+
loadIssuer
|
|
11
|
+
} from "./chunk-DMXYEWYY.js";
|
|
12
|
+
|
|
13
|
+
// src/hono.ts
|
|
14
|
+
import { Hono } from "hono";
|
|
15
|
+
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
|
|
16
|
+
var TXN_COOKIE = "gx_txn";
|
|
17
|
+
var SESSION_COOKIE = "gx_sess";
|
|
18
|
+
function godxSso(getConfig) {
|
|
19
|
+
const client = (c) => new GodxSsoClient(getConfig(c.env));
|
|
20
|
+
const cfg = (c) => getConfig(c.env);
|
|
21
|
+
const secure = (c) => c.req.url.startsWith("https:");
|
|
22
|
+
const routes = new Hono();
|
|
23
|
+
routes.get("/login", async (c) => {
|
|
24
|
+
const config = cfg(c);
|
|
25
|
+
const pkce = await createPkce();
|
|
26
|
+
const state = randomToken();
|
|
27
|
+
const nonce = randomToken();
|
|
28
|
+
const organizationContextId = c.req.query("organization_context_id") ?? config.defaultOrganizationContextId;
|
|
29
|
+
setCookie(c, TXN_COOKIE, await sealData({ v: pkce.verifier, s: state }, config.sessionSecret, 300), {
|
|
30
|
+
httpOnly: true,
|
|
31
|
+
secure: secure(c),
|
|
32
|
+
sameSite: "Lax",
|
|
33
|
+
path: "/",
|
|
34
|
+
maxAge: 300
|
|
35
|
+
});
|
|
36
|
+
return c.redirect(
|
|
37
|
+
await client(c).authorizeUrl({
|
|
38
|
+
state,
|
|
39
|
+
nonce,
|
|
40
|
+
codeChallenge: pkce.challenge,
|
|
41
|
+
scope: config.scope,
|
|
42
|
+
organizationContextId
|
|
43
|
+
})
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
const failRedirect = (c, error, description) => c.redirect(
|
|
47
|
+
`/?sso_error=${encodeURIComponent(error)}&sso_error_description=${encodeURIComponent(description)}`
|
|
48
|
+
);
|
|
49
|
+
routes.get("/callback", async (c) => {
|
|
50
|
+
const config = cfg(c);
|
|
51
|
+
const platformError = c.req.query("error");
|
|
52
|
+
if (platformError) {
|
|
53
|
+
return failRedirect(
|
|
54
|
+
c,
|
|
55
|
+
platformError,
|
|
56
|
+
c.req.query("error_description") ?? `\u30B5\u30A4\u30F3\u30A4\u30F3\u304C\u62D2\u5426\u3055\u308C\u307E\u3057\u305F\uFF08${platformError}\uFF09\u3002`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const code = c.req.query("code");
|
|
60
|
+
const state = c.req.query("state");
|
|
61
|
+
const txnCookie = getCookie(c, TXN_COOKIE);
|
|
62
|
+
deleteCookie(c, TXN_COOKIE, { path: "/" });
|
|
63
|
+
const txn = txnCookie ? await openData(txnCookie, config.sessionSecret) : null;
|
|
64
|
+
if (!code || !state || !txn || txn.s !== state) {
|
|
65
|
+
return failRedirect(
|
|
66
|
+
c,
|
|
67
|
+
"transaction_invalid",
|
|
68
|
+
"\u30ED\u30B0\u30A4\u30F3\u51E6\u7406\u306E\u6709\u52B9\u671F\u9650\u304C\u5207\u308C\u305F\u304B\u3001\u9014\u4E2D\u3067\u4E2D\u65AD\u3055\u308C\u307E\u3057\u305F\u3002\u3082\u3046\u4E00\u5EA6\u30B5\u30A4\u30F3\u30A4\u30F3\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
let tokens;
|
|
72
|
+
try {
|
|
73
|
+
tokens = await client(c).exchangeCode({ code, codeVerifier: txn.v });
|
|
74
|
+
} catch (e) {
|
|
75
|
+
return failRedirect(
|
|
76
|
+
c,
|
|
77
|
+
"token_exchange_failed",
|
|
78
|
+
`\u8A8D\u53EF\u30B3\u30FC\u30C9\u306E\u4EA4\u63DB\u306B\u5931\u6557\u3057\u307E\u3057\u305F\uFF08\u8A8D\u53EF\u30B3\u30FC\u30C9\u306F\u4E00\u5EA6\u3057\u304B\u4F7F\u3048\u307E\u305B\u3093\uFF09\u3002\u3082\u3046\u4E00\u5EA6\u30B5\u30A4\u30F3\u30A4\u30F3\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u8A73\u7D30: ${e instanceof Error ? e.message : String(e)}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
let verified;
|
|
82
|
+
try {
|
|
83
|
+
verified = await verifyAccessToken(tokens.access_token, config);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
return failRedirect(
|
|
86
|
+
c,
|
|
87
|
+
"token_verification_failed",
|
|
88
|
+
`\u30C8\u30FC\u30AF\u30F3\u306E\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002\u30B5\u30FC\u30D3\u30B9\u8A2D\u5B9A\uFF08issuer / service_slug\uFF09\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u8A73\u7D30: ${e instanceof Error ? e.message : String(e)}`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
let principal = verified;
|
|
92
|
+
try {
|
|
93
|
+
const info = await client(c).userinfo(tokens.access_token);
|
|
94
|
+
principal = Object.freeze({ ...verified, name: info.name, email: info.email });
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
const ttl = Math.max(0, principal.expiresAt - Math.floor(Date.now() / 1e3));
|
|
98
|
+
setCookie(
|
|
99
|
+
c,
|
|
100
|
+
SESSION_COOKIE,
|
|
101
|
+
await sealData({ p: principal, at: tokens.access_token }, config.sessionSecret, ttl),
|
|
102
|
+
{ httpOnly: true, secure: secure(c), sameSite: "Lax", path: "/", maxAge: ttl }
|
|
103
|
+
);
|
|
104
|
+
return c.redirect("/");
|
|
105
|
+
});
|
|
106
|
+
routes.get("/logout", async (c) => {
|
|
107
|
+
const config = cfg(c);
|
|
108
|
+
const raw = getCookie(c, SESSION_COOKIE);
|
|
109
|
+
deleteCookie(c, SESSION_COOKIE, { path: "/" });
|
|
110
|
+
if (raw) {
|
|
111
|
+
const session = await openData(raw, config.sessionSecret);
|
|
112
|
+
if (session) await client(c).revoke(session.at).catch(() => {
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const { metadata } = await loadIssuer(config.issuer);
|
|
116
|
+
return c.redirect(metadata.end_session_endpoint ?? "/");
|
|
117
|
+
});
|
|
118
|
+
const requireAuth = async (c, next) => {
|
|
119
|
+
const raw = getCookie(c, SESSION_COOKIE);
|
|
120
|
+
const session = raw ? await openData(raw, cfg(c).sessionSecret) : null;
|
|
121
|
+
if (!session || session.p.expiresAt <= Math.floor(Date.now() / 1e3)) {
|
|
122
|
+
return c.json({ error: "unauthenticated" }, 401);
|
|
123
|
+
}
|
|
124
|
+
c.set("principal", session.p);
|
|
125
|
+
c.set("accessToken", session.at);
|
|
126
|
+
await next();
|
|
127
|
+
};
|
|
128
|
+
const dockProxy = async (c) => {
|
|
129
|
+
const config = cfg(c);
|
|
130
|
+
const url = new URL(c.req.url);
|
|
131
|
+
const target = new URL(url.pathname + url.search, config.issuer);
|
|
132
|
+
const method = c.req.method;
|
|
133
|
+
const res = await fetch(target, {
|
|
134
|
+
method,
|
|
135
|
+
headers: {
|
|
136
|
+
Authorization: `Bearer ${c.get("accessToken")}`,
|
|
137
|
+
Accept: "application/json",
|
|
138
|
+
"Content-Type": c.req.header("Content-Type") ?? "application/json"
|
|
139
|
+
},
|
|
140
|
+
body: method === "GET" || method === "HEAD" ? void 0 : await c.req.arrayBuffer()
|
|
141
|
+
});
|
|
142
|
+
const headers = new Headers({ "Content-Type": res.headers.get("Content-Type") ?? "application/json" });
|
|
143
|
+
const requestId = res.headers.get("X-Request-Id");
|
|
144
|
+
if (requestId) headers.set("X-Request-Id", requestId);
|
|
145
|
+
return new Response(res.body, { status: res.status, headers });
|
|
146
|
+
};
|
|
147
|
+
const requireWritable = async (c, next) => {
|
|
148
|
+
const safe = c.req.method === "GET" || c.req.method === "HEAD" || c.req.method === "OPTIONS";
|
|
149
|
+
if (!safe && c.get("principal")?.accessMode !== "full") {
|
|
150
|
+
return c.json({ error: "organization is read-only" }, 403);
|
|
151
|
+
}
|
|
152
|
+
await next();
|
|
153
|
+
};
|
|
154
|
+
return { routes, requireAuth, requireWritable, dockProxy };
|
|
155
|
+
}
|
|
156
|
+
export {
|
|
157
|
+
godxSso
|
|
158
|
+
};
|
|
159
|
+
//# sourceMappingURL=hono.js.map
|
package/dist/hono.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hono.ts"],"sourcesContent":["/**\n * ./hono — thin adapter over ./auth + ./http for Hono apps on Workers.\n * Mounts the browser SSO routes and exposes the auth middlewares.\n * Everything platform-shaped stays in the core modules.\n */\nimport { Hono } from \"hono\";\nimport type { Context, Env as HonoEnv, MiddlewareHandler } from \"hono\";\nimport { deleteCookie, getCookie, setCookie } from \"hono/cookie\";\nimport { createPkce, openData, randomToken, sealData, verifyAccessToken } from \"./auth\";\nimport type { Principal } from \"./auth\";\nimport { GodxSsoClient, loadIssuer } from \"./http\";\n\nexport interface GodxSsoConfig {\n issuer: string;\n serviceSlug: string;\n clientId: string;\n clientSecret?: string;\n redirectUri: string;\n sessionSecret: string;\n scope?: string;\n /** Token audience = your ServiceInstance slug (defaults to serviceSlug). */\n audience?: string;\n /**\n * console_organization_id to fall back on when the login was not started\n * from a platform launch URL (which carries organization_context_id).\n * Useful in local dev; leave unset in production multi-org setups.\n */\n defaultOrganizationContextId?: string;\n}\n\ntype SessionData = { p: Principal; at: string };\n\nconst TXN_COOKIE = \"gx_txn\";\nconst SESSION_COOKIE = \"gx_sess\";\n\nexport type GodxVariables = { principal: Principal; accessToken: string };\n\nexport function godxSso<E extends HonoEnv & { Variables: GodxVariables }>(\n getConfig: (env: E[\"Bindings\"]) => GodxSsoConfig,\n) {\n const client = (c: Context<E>) => new GodxSsoClient(getConfig(c.env));\n const cfg = (c: Context<E>) => getConfig(c.env);\n const secure = (c: Context<E>) => c.req.url.startsWith(\"https:\");\n\n const routes = new Hono<E>();\n\n routes.get(\"/login\", async (c) => {\n const config = cfg(c);\n const pkce = await createPkce();\n const state = randomToken();\n const nonce = randomToken();\n // The platform launch URL carries organization_context_id; forward it, or\n // fall back to the configured default (single-org / local dev).\n const organizationContextId =\n c.req.query(\"organization_context_id\") ?? config.defaultOrganizationContextId;\n setCookie(c, TXN_COOKIE, await sealData({ v: pkce.verifier, s: state }, config.sessionSecret, 300), {\n httpOnly: true,\n secure: secure(c),\n sameSite: \"Lax\",\n path: \"/\",\n maxAge: 300,\n });\n return c.redirect(\n await client(c).authorizeUrl({\n state,\n nonce,\n codeChallenge: pkce.challenge,\n scope: config.scope,\n organizationContextId,\n }),\n );\n });\n\n // Every failure lands back on the app with a human-readable description —\n // the SPA renders it; a bare 400 helps nobody.\n const failRedirect = (c: Context<E>, error: string, description: string) =>\n c.redirect(\n `/?sso_error=${encodeURIComponent(error)}&sso_error_description=${encodeURIComponent(description)}`,\n );\n\n routes.get(\"/callback\", async (c) => {\n const config = cfg(c);\n\n // The platform denied the launch (access_denied etc.) and already wrote a\n // localized, human-readable explanation into error_description. Show it.\n const platformError = c.req.query(\"error\");\n if (platformError) {\n return failRedirect(\n c,\n platformError,\n c.req.query(\"error_description\") ?? `サインインが拒否されました(${platformError})。`,\n );\n }\n\n const code = c.req.query(\"code\");\n const state = c.req.query(\"state\");\n const txnCookie = getCookie(c, TXN_COOKIE);\n deleteCookie(c, TXN_COOKIE, { path: \"/\" });\n\n const txn = txnCookie ? await openData<{ v: string; s: string }>(txnCookie, config.sessionSecret) : null;\n if (!code || !state || !txn || txn.s !== state) {\n return failRedirect(\n c,\n \"transaction_invalid\",\n \"ログイン処理の有効期限が切れたか、途中で中断されました。もう一度サインインしてください。\",\n );\n }\n\n let tokens;\n try {\n tokens = await client(c).exchangeCode({ code, codeVerifier: txn.v });\n } catch (e) {\n return failRedirect(\n c,\n \"token_exchange_failed\",\n `認可コードの交換に失敗しました(認可コードは一度しか使えません)。もう一度サインインしてください。詳細: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n\n let verified;\n try {\n verified = await verifyAccessToken(tokens.access_token, config);\n } catch (e) {\n return failRedirect(\n c,\n \"token_verification_failed\",\n `トークンの検証に失敗しました。サービス設定(issuer / service_slug)を確認してください。詳細: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n\n // One userinfo call at login so the UI has a display name without further round-trips.\n let principal = verified;\n try {\n const info = await client(c).userinfo(tokens.access_token);\n principal = Object.freeze({ ...verified, name: info.name, email: info.email });\n } catch {\n // profile display data is optional; the verified principal stands on its own\n }\n\n const ttl = Math.max(0, principal.expiresAt - Math.floor(Date.now() / 1000));\n setCookie(\n c,\n SESSION_COOKIE,\n await sealData({ p: principal, at: tokens.access_token } satisfies SessionData, config.sessionSecret, ttl),\n { httpOnly: true, secure: secure(c), sameSite: \"Lax\", path: \"/\", maxAge: ttl },\n );\n return c.redirect(\"/\");\n });\n\n routes.get(\"/logout\", async (c) => {\n const config = cfg(c);\n const raw = getCookie(c, SESSION_COOKIE);\n deleteCookie(c, SESSION_COOKIE, { path: \"/\" });\n if (raw) {\n const session = await openData<SessionData>(raw, config.sessionSecret);\n if (session) await client(c).revoke(session.at).catch(() => {});\n }\n const { metadata } = await loadIssuer(config.issuer);\n return c.redirect(metadata.end_session_endpoint ?? \"/\");\n });\n\n const requireAuth: MiddlewareHandler<E> = async (c, next) => {\n const raw = getCookie(c, SESSION_COOKIE);\n const session = raw ? await openData<SessionData>(raw, cfg(c).sessionSecret) : null;\n if (!session || session.p.expiresAt <= Math.floor(Date.now() / 1000)) {\n return c.json({ error: \"unauthenticated\" }, 401);\n }\n c.set(\"principal\", session.p);\n c.set(\"accessToken\", session.at);\n await next();\n };\n\n /**\n * Forwards /api/dock/v1/* to the platform with the session's Bearer token —\n * the GoDX Dock's `apiBase` lane: no CORS, no token in the page. Mount it\n * BEHIND requireAuth and BEFORE any requireWritable (Dock actions are the\n * platform's calls to authorize, not this app's writes).\n */\n const dockProxy: MiddlewareHandler<E> = async (c) => {\n const config = cfg(c);\n const url = new URL(c.req.url);\n const target = new URL(url.pathname + url.search, config.issuer);\n const method = c.req.method;\n const res = await fetch(target, {\n method,\n headers: {\n Authorization: `Bearer ${c.get(\"accessToken\")}`,\n Accept: \"application/json\",\n \"Content-Type\": c.req.header(\"Content-Type\") ?? \"application/json\",\n },\n body: method === \"GET\" || method === \"HEAD\" ? undefined : await c.req.arrayBuffer(),\n });\n const headers = new Headers({ \"Content-Type\": res.headers.get(\"Content-Type\") ?? \"application/json\" });\n const requestId = res.headers.get(\"X-Request-Id\");\n if (requestId) headers.set(\"X-Request-Id\", requestId);\n return new Response(res.body, { status: res.status, headers });\n };\n\n /** Enforces organization_access_mode: read-only orgs may not write. */\n const requireWritable: MiddlewareHandler<E> = async (c, next) => {\n const safe = c.req.method === \"GET\" || c.req.method === \"HEAD\" || c.req.method === \"OPTIONS\";\n if (!safe && c.get(\"principal\")?.accessMode !== \"full\") {\n return c.json({ error: \"organization is read-only\" }, 403);\n }\n await next();\n };\n\n return { routes, requireAuth, requireWritable, dockProxy };\n}\n"],"mappings":"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","names":[]}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createRemoteJWKSet } from 'jose';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ./http — endpoint contract from OIDC discovery, the SSO client, and the
|
|
5
|
+
* server-to-server service API. Runtime-agnostic: fetch + WebCrypto only.
|
|
6
|
+
* Mirrors php-sdk's Http module boundary.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
interface IssuerMetadata {
|
|
10
|
+
issuer: string;
|
|
11
|
+
authorization_endpoint: string;
|
|
12
|
+
token_endpoint: string;
|
|
13
|
+
userinfo_endpoint: string;
|
|
14
|
+
jwks_uri: string;
|
|
15
|
+
end_session_endpoint?: string;
|
|
16
|
+
revocation_endpoint?: string;
|
|
17
|
+
}
|
|
18
|
+
interface Issuer {
|
|
19
|
+
metadata: IssuerMetadata;
|
|
20
|
+
jwks: ReturnType<typeof createRemoteJWKSet>;
|
|
21
|
+
}
|
|
22
|
+
declare class GodxHttpError extends Error {
|
|
23
|
+
readonly status: number;
|
|
24
|
+
readonly body: string;
|
|
25
|
+
constructor(status: number, body: string, message?: string);
|
|
26
|
+
}
|
|
27
|
+
declare function loadIssuer(issuerUrl: string): Promise<Issuer>;
|
|
28
|
+
interface SsoClientConfig {
|
|
29
|
+
issuer: string;
|
|
30
|
+
serviceSlug: string;
|
|
31
|
+
clientId: string;
|
|
32
|
+
/** Omit for a public (secretless) instance — PKCE alone then. */
|
|
33
|
+
clientSecret?: string;
|
|
34
|
+
redirectUri: string;
|
|
35
|
+
}
|
|
36
|
+
interface TokenResponse {
|
|
37
|
+
access_token: string;
|
|
38
|
+
token_type: string;
|
|
39
|
+
expires_in: number;
|
|
40
|
+
id_token?: string;
|
|
41
|
+
refresh_token?: string;
|
|
42
|
+
scope?: string;
|
|
43
|
+
}
|
|
44
|
+
interface UserinfoResponse {
|
|
45
|
+
sub: string;
|
|
46
|
+
email?: string;
|
|
47
|
+
email_verified?: boolean;
|
|
48
|
+
name?: string;
|
|
49
|
+
locale?: string;
|
|
50
|
+
timezone?: string;
|
|
51
|
+
}
|
|
52
|
+
declare class GodxSsoClient {
|
|
53
|
+
readonly config: SsoClientConfig;
|
|
54
|
+
constructor(config: SsoClientConfig);
|
|
55
|
+
authorizeUrl(params: {
|
|
56
|
+
state: string;
|
|
57
|
+
nonce: string;
|
|
58
|
+
codeChallenge: string;
|
|
59
|
+
scope?: string;
|
|
60
|
+
/** console_organization_id — required by the launch gates; usually arrives on the launch URL. */
|
|
61
|
+
organizationContextId?: string;
|
|
62
|
+
}): Promise<string>;
|
|
63
|
+
exchangeCode(params: {
|
|
64
|
+
code: string;
|
|
65
|
+
codeVerifier: string;
|
|
66
|
+
}): Promise<TokenResponse>;
|
|
67
|
+
refresh(refreshToken: string): Promise<TokenResponse>;
|
|
68
|
+
/** Best-effort revocation; failures are the caller's decision to ignore. */
|
|
69
|
+
revoke(token: string): Promise<void>;
|
|
70
|
+
userinfo(accessToken: string): Promise<UserinfoResponse>;
|
|
71
|
+
private tokenRequest;
|
|
72
|
+
private postForm;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Server-to-server reads with service credentials (X-Service-Slug/X-Service-Secret).
|
|
76
|
+
* Rate-limited by the platform (429) — cache results and back off; no auto-retry here.
|
|
77
|
+
*/
|
|
78
|
+
declare function createServiceApi(config: {
|
|
79
|
+
issuer: string;
|
|
80
|
+
serviceSlug: string;
|
|
81
|
+
serviceSecret: string;
|
|
82
|
+
}): {
|
|
83
|
+
users: (q: {
|
|
84
|
+
organizationSlug: string;
|
|
85
|
+
perPage?: number;
|
|
86
|
+
page?: number;
|
|
87
|
+
}) => Promise<unknown>;
|
|
88
|
+
branches: (q: {
|
|
89
|
+
organizationSlug: string;
|
|
90
|
+
}) => Promise<unknown>;
|
|
91
|
+
brands: (q: {
|
|
92
|
+
organizationSlug: string;
|
|
93
|
+
}) => Promise<unknown>;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export { GodxHttpError, GodxSsoClient, type Issuer, type IssuerMetadata, type SsoClientConfig, type TokenResponse, type UserinfoResponse, createServiceApi, loadIssuer };
|
package/dist/http.js
ADDED
package/dist/http.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|