@broberg/sso 0.13.0 → 0.14.1
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 +18 -0
- package/dist/index.cjs +144 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +47 -1
- package/dist/index.d.ts +47 -1
- package/dist/index.js +139 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -422,6 +422,52 @@ interface CreateSsoClientOptions {
|
|
|
422
422
|
}
|
|
423
423
|
declare function createSsoClient(config: SsoConfig, options?: CreateSsoClientOptions): SsoClient;
|
|
424
424
|
|
|
425
|
+
declare const DEFAULT_ISSUER = "https://id.broberg.ai";
|
|
426
|
+
/** No platform identity to exchange: not on Fly, not in a GitHub Actions job with id-token: write. */
|
|
427
|
+
declare class NoWorkloadIdentityError extends Error {
|
|
428
|
+
constructor(message: string);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* BID refused the exchange. `code` is BID's own error name (the part of
|
|
432
|
+
* error_description before the colon, e.g. `no_rule_for_audience`), `status` the
|
|
433
|
+
* HTTP status. A 4xx is PERMANENT for this request — fix the rule, do not retry.
|
|
434
|
+
*/
|
|
435
|
+
declare class TicketExchangeError extends Error {
|
|
436
|
+
readonly code: string;
|
|
437
|
+
readonly status: number;
|
|
438
|
+
constructor(message: string, code: string, status: number);
|
|
439
|
+
}
|
|
440
|
+
/** BID or the platform could not be reached, or answered 5xx. TRANSIENT — retry later. */
|
|
441
|
+
declare class TicketUnavailableError extends Error {
|
|
442
|
+
constructor(message: string);
|
|
443
|
+
}
|
|
444
|
+
interface TicketClientOptions {
|
|
445
|
+
issuer?: string;
|
|
446
|
+
/** Renew this many seconds before exp. Default 30. */
|
|
447
|
+
renewBeforeSec?: number;
|
|
448
|
+
/** Injectable for tests. */
|
|
449
|
+
env?: Record<string, string | undefined>;
|
|
450
|
+
fetchImpl?: typeof fetch;
|
|
451
|
+
now?: () => number;
|
|
452
|
+
/** Ask Fly's machine API for an OIDC token. Default: POST over the /.fly/api unix socket. */
|
|
453
|
+
flyOidc?: (aud: string) => Promise<string>;
|
|
454
|
+
/** Whether the Fly socket is present. Default: checks /.fly/api. */
|
|
455
|
+
onFly?: () => boolean;
|
|
456
|
+
}
|
|
457
|
+
interface TicketClient {
|
|
458
|
+
/** A ticket for `audience` (BID's short name, e.g. "discovery"), from cache while it is fresh. */
|
|
459
|
+
get(opts: {
|
|
460
|
+
audience: string;
|
|
461
|
+
scope?: string;
|
|
462
|
+
}): Promise<string>;
|
|
463
|
+
}
|
|
464
|
+
declare function createTicketClient(options?: TicketClientOptions): TicketClient;
|
|
465
|
+
/** A ticket from the process-wide client (one cache per process). */
|
|
466
|
+
declare function fetchTicket(opts: {
|
|
467
|
+
audience: string;
|
|
468
|
+
scope?: string;
|
|
469
|
+
}): Promise<string>;
|
|
470
|
+
|
|
425
471
|
type PrincipalType = "human" | "service" | "agent";
|
|
426
472
|
/** What a verified ticket tells the receiver — a small, typed principal, never the raw payload. */
|
|
427
473
|
interface TicketPrincipal {
|
|
@@ -624,4 +670,4 @@ declare function memoryTokenStore(options?: {
|
|
|
624
670
|
readonly size: number;
|
|
625
671
|
};
|
|
626
672
|
|
|
627
|
-
export { AVATAR_TYPES, type AddressOwnership, type BeginLoginOptions, type BidProfile, type CreateSsoClientOptions, DEFAULT_SESSION_MAX_AGE, type Discovery, type InvitationOutcome, type InvitationResult, type InviteUsersInput, type JwksCache, type JwksCacheOptions, JwksError, JwksUnavailableError, JwksUnknownKeyError, type LoginResult, type LoginStart, type LogoutToken, MAX_AVATAR_BYTES, MAX_INVITATIONS, type MigrationState, type MigrationStatus, type PrincipalType, type SessionPayload, SsoAppApiError, SsoAvatarRejectedError, type SsoClaims, type SsoClient, type SsoConfig, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoReauthError, type TicketPrincipal, type TicketVerifier, type TicketVerifierOptions, type TokenSet, type TokenStore, cookieHeader, createJwksCache, createSsoClient, createTicketVerifier, loadSsoConfig, memoryTokenStore, readCookie, signSession, signValue, verifySession, verifyValue };
|
|
673
|
+
export { AVATAR_TYPES, type AddressOwnership, type BeginLoginOptions, type BidProfile, type CreateSsoClientOptions, DEFAULT_ISSUER, DEFAULT_SESSION_MAX_AGE, type Discovery, type InvitationOutcome, type InvitationResult, type InviteUsersInput, type JwksCache, type JwksCacheOptions, JwksError, JwksUnavailableError, JwksUnknownKeyError, type LoginResult, type LoginStart, type LogoutToken, MAX_AVATAR_BYTES, MAX_INVITATIONS, type MigrationState, type MigrationStatus, NoWorkloadIdentityError, type PrincipalType, type SessionPayload, SsoAppApiError, SsoAvatarRejectedError, type SsoClaims, type SsoClient, type SsoConfig, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoReauthError, type TicketClient, type TicketClientOptions, TicketExchangeError, type TicketPrincipal, TicketUnavailableError, type TicketVerifier, type TicketVerifierOptions, type TokenSet, type TokenStore, cookieHeader, createJwksCache, createSsoClient, createTicketClient, createTicketVerifier, fetchTicket, loadSsoConfig, memoryTokenStore, readCookie, signSession, signValue, verifySession, verifyValue };
|
package/dist/index.d.ts
CHANGED
|
@@ -422,6 +422,52 @@ interface CreateSsoClientOptions {
|
|
|
422
422
|
}
|
|
423
423
|
declare function createSsoClient(config: SsoConfig, options?: CreateSsoClientOptions): SsoClient;
|
|
424
424
|
|
|
425
|
+
declare const DEFAULT_ISSUER = "https://id.broberg.ai";
|
|
426
|
+
/** No platform identity to exchange: not on Fly, not in a GitHub Actions job with id-token: write. */
|
|
427
|
+
declare class NoWorkloadIdentityError extends Error {
|
|
428
|
+
constructor(message: string);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* BID refused the exchange. `code` is BID's own error name (the part of
|
|
432
|
+
* error_description before the colon, e.g. `no_rule_for_audience`), `status` the
|
|
433
|
+
* HTTP status. A 4xx is PERMANENT for this request — fix the rule, do not retry.
|
|
434
|
+
*/
|
|
435
|
+
declare class TicketExchangeError extends Error {
|
|
436
|
+
readonly code: string;
|
|
437
|
+
readonly status: number;
|
|
438
|
+
constructor(message: string, code: string, status: number);
|
|
439
|
+
}
|
|
440
|
+
/** BID or the platform could not be reached, or answered 5xx. TRANSIENT — retry later. */
|
|
441
|
+
declare class TicketUnavailableError extends Error {
|
|
442
|
+
constructor(message: string);
|
|
443
|
+
}
|
|
444
|
+
interface TicketClientOptions {
|
|
445
|
+
issuer?: string;
|
|
446
|
+
/** Renew this many seconds before exp. Default 30. */
|
|
447
|
+
renewBeforeSec?: number;
|
|
448
|
+
/** Injectable for tests. */
|
|
449
|
+
env?: Record<string, string | undefined>;
|
|
450
|
+
fetchImpl?: typeof fetch;
|
|
451
|
+
now?: () => number;
|
|
452
|
+
/** Ask Fly's machine API for an OIDC token. Default: POST over the /.fly/api unix socket. */
|
|
453
|
+
flyOidc?: (aud: string) => Promise<string>;
|
|
454
|
+
/** Whether the Fly socket is present. Default: checks /.fly/api. */
|
|
455
|
+
onFly?: () => boolean;
|
|
456
|
+
}
|
|
457
|
+
interface TicketClient {
|
|
458
|
+
/** A ticket for `audience` (BID's short name, e.g. "discovery"), from cache while it is fresh. */
|
|
459
|
+
get(opts: {
|
|
460
|
+
audience: string;
|
|
461
|
+
scope?: string;
|
|
462
|
+
}): Promise<string>;
|
|
463
|
+
}
|
|
464
|
+
declare function createTicketClient(options?: TicketClientOptions): TicketClient;
|
|
465
|
+
/** A ticket from the process-wide client (one cache per process). */
|
|
466
|
+
declare function fetchTicket(opts: {
|
|
467
|
+
audience: string;
|
|
468
|
+
scope?: string;
|
|
469
|
+
}): Promise<string>;
|
|
470
|
+
|
|
425
471
|
type PrincipalType = "human" | "service" | "agent";
|
|
426
472
|
/** What a verified ticket tells the receiver — a small, typed principal, never the raw payload. */
|
|
427
473
|
interface TicketPrincipal {
|
|
@@ -624,4 +670,4 @@ declare function memoryTokenStore(options?: {
|
|
|
624
670
|
readonly size: number;
|
|
625
671
|
};
|
|
626
672
|
|
|
627
|
-
export { AVATAR_TYPES, type AddressOwnership, type BeginLoginOptions, type BidProfile, type CreateSsoClientOptions, DEFAULT_SESSION_MAX_AGE, type Discovery, type InvitationOutcome, type InvitationResult, type InviteUsersInput, type JwksCache, type JwksCacheOptions, JwksError, JwksUnavailableError, JwksUnknownKeyError, type LoginResult, type LoginStart, type LogoutToken, MAX_AVATAR_BYTES, MAX_INVITATIONS, type MigrationState, type MigrationStatus, type PrincipalType, type SessionPayload, SsoAppApiError, SsoAvatarRejectedError, type SsoClaims, type SsoClient, type SsoConfig, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoReauthError, type TicketPrincipal, type TicketVerifier, type TicketVerifierOptions, type TokenSet, type TokenStore, cookieHeader, createJwksCache, createSsoClient, createTicketVerifier, loadSsoConfig, memoryTokenStore, readCookie, signSession, signValue, verifySession, verifyValue };
|
|
673
|
+
export { AVATAR_TYPES, type AddressOwnership, type BeginLoginOptions, type BidProfile, type CreateSsoClientOptions, DEFAULT_ISSUER, DEFAULT_SESSION_MAX_AGE, type Discovery, type InvitationOutcome, type InvitationResult, type InviteUsersInput, type JwksCache, type JwksCacheOptions, JwksError, JwksUnavailableError, JwksUnknownKeyError, type LoginResult, type LoginStart, type LogoutToken, MAX_AVATAR_BYTES, MAX_INVITATIONS, type MigrationState, type MigrationStatus, NoWorkloadIdentityError, type PrincipalType, type SessionPayload, SsoAppApiError, SsoAvatarRejectedError, type SsoClaims, type SsoClient, type SsoConfig, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoReauthError, type TicketClient, type TicketClientOptions, TicketExchangeError, type TicketPrincipal, TicketUnavailableError, type TicketVerifier, type TicketVerifierOptions, type TokenSet, type TokenStore, cookieHeader, createJwksCache, createSsoClient, createTicketClient, createTicketVerifier, fetchTicket, loadSsoConfig, memoryTokenStore, readCookie, signSession, signValue, verifySession, verifyValue };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { importJWK, decodeProtectedHeader, jwtVerify } from 'jose';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { request } from 'http';
|
|
2
4
|
|
|
3
5
|
// src/config.ts
|
|
4
6
|
var SsoConfigError = class extends Error {
|
|
@@ -664,6 +666,142 @@ function createSsoClient(config, options = {}) {
|
|
|
664
666
|
}
|
|
665
667
|
};
|
|
666
668
|
}
|
|
669
|
+
var DEFAULT_ISSUER = "https://id.broberg.ai";
|
|
670
|
+
var FLY_SOCKET = "/.fly/api";
|
|
671
|
+
var NoWorkloadIdentityError = class extends Error {
|
|
672
|
+
constructor(message) {
|
|
673
|
+
super(message);
|
|
674
|
+
this.name = "NoWorkloadIdentityError";
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
var TicketExchangeError = class extends Error {
|
|
678
|
+
constructor(message, code, status) {
|
|
679
|
+
super(message);
|
|
680
|
+
this.code = code;
|
|
681
|
+
this.status = status;
|
|
682
|
+
this.name = "TicketExchangeError";
|
|
683
|
+
}
|
|
684
|
+
code;
|
|
685
|
+
status;
|
|
686
|
+
};
|
|
687
|
+
var TicketUnavailableError = class extends Error {
|
|
688
|
+
constructor(message) {
|
|
689
|
+
super(message);
|
|
690
|
+
this.name = "TicketUnavailableError";
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
function flyOidcOverSocket(aud) {
|
|
694
|
+
return new Promise((resolve, reject) => {
|
|
695
|
+
const body = JSON.stringify({ aud });
|
|
696
|
+
const req = request(
|
|
697
|
+
{ socketPath: FLY_SOCKET, path: "/v1/tokens/oidc", method: "POST", headers: { "content-type": "application/json", "content-length": Buffer.byteLength(body) } },
|
|
698
|
+
(res) => {
|
|
699
|
+
let data = "";
|
|
700
|
+
res.setEncoding("utf8");
|
|
701
|
+
res.on("data", (c) => data += c);
|
|
702
|
+
res.on("end", () => {
|
|
703
|
+
if ((res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300) resolve(data.trim());
|
|
704
|
+
else reject(new TicketUnavailableError(`Fly OIDC answered ${res.statusCode}`));
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
);
|
|
708
|
+
req.on("error", (e) => reject(new TicketUnavailableError(`Fly OIDC socket: ${e.message}`)));
|
|
709
|
+
req.end(body);
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
function jwtExp(token) {
|
|
713
|
+
try {
|
|
714
|
+
const payload = JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64url").toString("utf8"));
|
|
715
|
+
return typeof payload.exp === "number" ? payload.exp : null;
|
|
716
|
+
} catch {
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function createTicketClient(options = {}) {
|
|
721
|
+
const issuer = (options.issuer ?? DEFAULT_ISSUER).replace(/\/+$/, "");
|
|
722
|
+
const renewBeforeSec = options.renewBeforeSec ?? 30;
|
|
723
|
+
const env = options.env ?? process.env;
|
|
724
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
725
|
+
const now = options.now ?? (() => Date.now());
|
|
726
|
+
const flyOidc = options.flyOidc ?? flyOidcOverSocket;
|
|
727
|
+
const onFly = options.onFly ?? (() => !!env.FLY_APP_NAME && existsSync(FLY_SOCKET));
|
|
728
|
+
const cache = /* @__PURE__ */ new Map();
|
|
729
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
730
|
+
async function platformToken() {
|
|
731
|
+
if (onFly()) return flyOidc(issuer);
|
|
732
|
+
const url = env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
733
|
+
const bearer = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
734
|
+
if (url && bearer) {
|
|
735
|
+
let res;
|
|
736
|
+
try {
|
|
737
|
+
res = await fetchImpl(`${url}&audience=${encodeURIComponent(issuer)}`, { headers: { authorization: `Bearer ${bearer}` } });
|
|
738
|
+
} catch (e) {
|
|
739
|
+
throw new TicketUnavailableError(`GitHub OIDC endpoint unreachable: ${e instanceof Error ? e.message : String(e)}`);
|
|
740
|
+
}
|
|
741
|
+
if (!res.ok) throw new TicketUnavailableError(`GitHub OIDC endpoint answered ${res.status}`);
|
|
742
|
+
const { value } = await res.json();
|
|
743
|
+
if (!value) throw new TicketUnavailableError("GitHub OIDC endpoint returned no token");
|
|
744
|
+
return value;
|
|
745
|
+
}
|
|
746
|
+
throw new NoWorkloadIdentityError(
|
|
747
|
+
"No workload identity to exchange for a ticket: not on a Fly machine (FLY_APP_NAME + /.fly/api) and not in a GitHub Actions job (ACTIONS_ID_TOKEN_REQUEST_URL/_TOKEN \u2014 add `permissions: id-token: write`). A Mac session keeps using its session key."
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
async function exchange(audience, scope) {
|
|
751
|
+
const subject = await platformToken();
|
|
752
|
+
const form = new URLSearchParams({
|
|
753
|
+
grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
754
|
+
subject_token: subject,
|
|
755
|
+
subject_token_type: "urn:ietf:params:oauth:token-type:jwt",
|
|
756
|
+
audience
|
|
757
|
+
});
|
|
758
|
+
if (scope) form.set("scope", scope);
|
|
759
|
+
let res;
|
|
760
|
+
try {
|
|
761
|
+
res = await fetchImpl(`${issuer}/oauth2/token`, {
|
|
762
|
+
method: "POST",
|
|
763
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
764
|
+
body: form
|
|
765
|
+
});
|
|
766
|
+
} catch (e) {
|
|
767
|
+
throw new TicketUnavailableError(`${issuer} unreachable: ${e instanceof Error ? e.message : String(e)}`);
|
|
768
|
+
}
|
|
769
|
+
let body = {};
|
|
770
|
+
try {
|
|
771
|
+
body = await res.json();
|
|
772
|
+
} catch {
|
|
773
|
+
}
|
|
774
|
+
if (res.status >= 500) throw new TicketUnavailableError(`${issuer} answered ${res.status}`);
|
|
775
|
+
if (!res.ok || !body.access_token) {
|
|
776
|
+
const desc = body.error_description ?? "";
|
|
777
|
+
const code = desc.includes(":") ? desc.slice(0, desc.indexOf(":")).trim() : body.error ?? "exchange_refused";
|
|
778
|
+
throw new TicketExchangeError(`Broberg ID refused the ticket for ${audience}: ${desc || body.error || res.status}`, code, res.status);
|
|
779
|
+
}
|
|
780
|
+
const nowSec = Math.floor(now() / 1e3);
|
|
781
|
+
const exp = jwtExp(body.access_token) ?? nowSec + (body.expires_in ?? 300);
|
|
782
|
+
return { token: body.access_token, exp };
|
|
783
|
+
}
|
|
784
|
+
return {
|
|
785
|
+
async get({ audience, scope }) {
|
|
786
|
+
const key = `${audience} ${scope ?? ""}`;
|
|
787
|
+
const hit = cache.get(key);
|
|
788
|
+
if (hit && hit.exp - renewBeforeSec > now() / 1e3) return hit.token;
|
|
789
|
+
const pending = inFlight.get(key);
|
|
790
|
+
if (pending) return pending;
|
|
791
|
+
const p = exchange(audience, scope).then((t) => {
|
|
792
|
+
cache.set(key, t);
|
|
793
|
+
return t.token;
|
|
794
|
+
}).finally(() => inFlight.delete(key));
|
|
795
|
+
inFlight.set(key, p);
|
|
796
|
+
return p;
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
var shared = null;
|
|
801
|
+
function fetchTicket(opts) {
|
|
802
|
+
shared ??= createTicketClient();
|
|
803
|
+
return shared.get(opts);
|
|
804
|
+
}
|
|
667
805
|
var PRINCIPAL_TYPES = ["human", "service", "agent"];
|
|
668
806
|
function createTicketVerifier(options) {
|
|
669
807
|
const {
|
|
@@ -881,6 +1019,6 @@ function memoryTokenStore(options = {}) {
|
|
|
881
1019
|
};
|
|
882
1020
|
}
|
|
883
1021
|
|
|
884
|
-
export { AVATAR_TYPES, DEFAULT_SESSION_MAX_AGE, JwksError, JwksUnavailableError, JwksUnknownKeyError, MAX_AVATAR_BYTES, MAX_INVITATIONS, SsoAppApiError, SsoAvatarRejectedError, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoReauthError, cookieHeader, createJwksCache, createSsoClient, createTicketVerifier, loadSsoConfig, memoryTokenStore, readCookie, signSession, signValue, verifySession, verifyValue };
|
|
1022
|
+
export { AVATAR_TYPES, DEFAULT_ISSUER, DEFAULT_SESSION_MAX_AGE, JwksError, JwksUnavailableError, JwksUnknownKeyError, MAX_AVATAR_BYTES, MAX_INVITATIONS, NoWorkloadIdentityError, SsoAppApiError, SsoAvatarRejectedError, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoReauthError, TicketExchangeError, TicketUnavailableError, cookieHeader, createJwksCache, createSsoClient, createTicketClient, createTicketVerifier, fetchTicket, loadSsoConfig, memoryTokenStore, readCookie, signSession, signValue, verifySession, verifyValue };
|
|
885
1023
|
//# sourceMappingURL=index.js.map
|
|
886
1024
|
//# sourceMappingURL=index.js.map
|