@andco/sdk 0.0.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/LICENSE +202 -0
- package/README.md +176 -0
- package/dist/auth.d.ts +71 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +98 -0
- package/dist/browser/controller.d.ts +76 -0
- package/dist/browser/controller.d.ts.map +1 -0
- package/dist/browser/controller.js +215 -0
- package/dist/browser/frame.d.ts +65 -0
- package/dist/browser/frame.d.ts.map +1 -0
- package/dist/browser/frame.js +237 -0
- package/dist/browser/index.d.ts +52 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +117 -0
- package/dist/browser/popup.d.ts +105 -0
- package/dist/browser/popup.d.ts.map +1 -0
- package/dist/browser/popup.js +177 -0
- package/dist/cli/index.d.ts +47 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +72 -0
- package/dist/cli/server.d.ts +11 -0
- package/dist/cli/server.d.ts.map +1 -0
- package/dist/cli/server.js +91 -0
- package/dist/client.d.ts +135 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +210 -0
- package/dist/config.d.ts +82 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +109 -0
- package/dist/credentials.d.ts +76 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +0 -0
- package/dist/errors.d.ts +165 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +197 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/intents.d.ts +89 -0
- package/dist/intents.d.ts.map +1 -0
- package/dist/intents.js +157 -0
- package/dist/oauth.d.ts +147 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +348 -0
- package/dist/presenter.d.ts +36 -0
- package/dist/presenter.d.ts.map +1 -0
- package/dist/presenter.js +1 -0
- package/dist/rest.d.ts +58 -0
- package/dist/rest.d.ts.map +1 -0
- package/dist/rest.js +48 -0
- package/dist/server/index.d.ts +21 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +27 -0
- package/dist/server-metadata.generated.d.ts +4 -0
- package/dist/server-metadata.generated.d.ts.map +1 -0
- package/dist/server-metadata.generated.js +49 -0
- package/dist/session-store.d.ts +83 -0
- package/dist/session-store.d.ts.map +1 -0
- package/dist/session-store.js +186 -0
- package/dist/storage.d.ts +61 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +47 -0
- package/package.json +56 -0
package/dist/oauth.d.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import * as oidc from "openid-client";
|
|
2
|
+
import type { AndcoConfig, AndcoScope } from "./config.js";
|
|
3
|
+
import type { AndcoSession, AndcoUser } from "./credentials.js";
|
|
4
|
+
import { Result } from "./errors.js";
|
|
5
|
+
/** An RFC 9396 authorization detail. Its shape is owned by the Resource Server that defines it. */
|
|
6
|
+
export type AndcoAuthorizationDetail = {
|
|
7
|
+
readonly type: string;
|
|
8
|
+
} & Readonly<Record<string, unknown>>;
|
|
9
|
+
/** One Resource Server's contribution to a single authorization request. */
|
|
10
|
+
export type AndcoResourceAuthorization = {
|
|
11
|
+
readonly resourceServerId: string;
|
|
12
|
+
readonly resource: string;
|
|
13
|
+
readonly scopes: readonly AndcoScope[];
|
|
14
|
+
readonly authorizationDetails?: readonly AndcoAuthorizationDetail[];
|
|
15
|
+
readonly orgId?: string;
|
|
16
|
+
};
|
|
17
|
+
/** How an authorization request reaches the Authorization Server. */
|
|
18
|
+
export type AndcoTransportMode = "auto" | "get" | "par";
|
|
19
|
+
/** Everything a caller may vary for one authorization request. */
|
|
20
|
+
export type AndcoAuthorizationOptions = {
|
|
21
|
+
redirectTo?: string | URL;
|
|
22
|
+
scopes?: readonly AndcoScope[];
|
|
23
|
+
/** Immutable contributions from Resource Server Definitions, composed into one request. */
|
|
24
|
+
authorizations?: readonly AndcoResourceAuthorization[];
|
|
25
|
+
resource?: string | readonly string[];
|
|
26
|
+
orgId?: string;
|
|
27
|
+
authorizationDetails?: readonly AndcoAuthorizationDetail[];
|
|
28
|
+
/** Opaque destination reference scoped to this OAuth Client. */
|
|
29
|
+
externalId?: string;
|
|
30
|
+
/** Correlates the callback with an already-open presentation. */
|
|
31
|
+
state?: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* A short-lived authorization transaction. The verifier must stay private until the exchange, so a
|
|
35
|
+
* caller persists this wherever the callback can recover it: a server session, a store entry, or a
|
|
36
|
+
* terminal's memory.
|
|
37
|
+
*/
|
|
38
|
+
export type AndcoAuthorizationRequest = {
|
|
39
|
+
authorizationUrl: URL;
|
|
40
|
+
codeVerifier: string;
|
|
41
|
+
state: string;
|
|
42
|
+
redirectTo: string;
|
|
43
|
+
createdAt: number;
|
|
44
|
+
};
|
|
45
|
+
/** RFC 8628 instructions a constrained client displays instead of opening a browser. */
|
|
46
|
+
export type AndcoDeviceAuthorization = {
|
|
47
|
+
deviceCode: string;
|
|
48
|
+
userCode: string;
|
|
49
|
+
verificationUri: string;
|
|
50
|
+
verificationUriComplete: string;
|
|
51
|
+
expiresIn: number;
|
|
52
|
+
interval: number;
|
|
53
|
+
/** The server response, kept so polling needs no reconstruction by the caller. */
|
|
54
|
+
raw: oidc.DeviceAuthorizationResponse;
|
|
55
|
+
};
|
|
56
|
+
export type AndcoOAuthOptions = {
|
|
57
|
+
config: AndcoConfig;
|
|
58
|
+
fetch: typeof globalThis.fetch;
|
|
59
|
+
/** Present for a confidential client, absent for a public one. */
|
|
60
|
+
clientSecret?: string | null;
|
|
61
|
+
transport?: AndcoTransportMode;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* The one OAuth implementation in the SDK.
|
|
65
|
+
*
|
|
66
|
+
* It replaces four parallel ones: the hand-rolled browser client, the separate confidential client,
|
|
67
|
+
* and the flows written by hand inside the Better Auth and Passport packages. Those diverged, which
|
|
68
|
+
* is exactly the failure a protocol implementation must not have. The work is delegated to
|
|
69
|
+
* `openid-client`, which carries no Node built-ins and therefore runs in browsers, Expo, and Tauri.
|
|
70
|
+
*
|
|
71
|
+
* Nothing here holds a session. A request is created, handed to whatever presents it, and exchanged
|
|
72
|
+
* with the transaction the caller kept.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* const { data: request } = await andco.oauth.createAuthorizationRequest({ scopes: ["email"] });
|
|
77
|
+
* const callbackUrl = await presentSomehow(request.authorizationUrl);
|
|
78
|
+
* const { data: session } = await andco.oauth.exchangeCallback({ callbackUrl, request });
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export declare class AndcoOAuth {
|
|
82
|
+
#private;
|
|
83
|
+
constructor(options: AndcoOAuthOptions);
|
|
84
|
+
/** Whether this client authenticates itself with a secret. */
|
|
85
|
+
get isConfidential(): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Prepares one Authorization Code transaction with S256 PKCE, and resolves the Authorization
|
|
88
|
+
* Transport Policy internally so a caller never chooses between a direct request and PAR.
|
|
89
|
+
*/
|
|
90
|
+
createAuthorizationRequest(options?: AndcoAuthorizationOptions): Promise<Result<AndcoAuthorizationRequest>>;
|
|
91
|
+
/**
|
|
92
|
+
* Validates a complete callback URL against the transaction that produced it, then exchanges the
|
|
93
|
+
* code. Validation happens here rather than in each presenter so every runtime — browser, Host
|
|
94
|
+
* bridge, terminal paste — gets the same checks.
|
|
95
|
+
*/
|
|
96
|
+
exchangeCallback(options: {
|
|
97
|
+
callbackUrl: string | URL;
|
|
98
|
+
request: AndcoAuthorizationRequest;
|
|
99
|
+
}): Promise<Result<AndcoSession>>;
|
|
100
|
+
/**
|
|
101
|
+
* Resolves an authorization request someone else already assembled.
|
|
102
|
+
*
|
|
103
|
+
* Exists for integrations that generate their own OAuth request — Better Auth and Passport both
|
|
104
|
+
* do — and still need the Authorization Transport Policy applied. Without it each one reimplements
|
|
105
|
+
* the direct-versus-PAR decision, which is how three implementations of it came to exist.
|
|
106
|
+
*
|
|
107
|
+
* `transport` overrides the instance's own policy for this call only — Passport exposes this as a
|
|
108
|
+
* per-strategy default, distinct from the client-wide policy every other caller gets.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* const { data: url } = await andco.oauth.resolveAuthorizationUrl(parametersFromBetterAuth);
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
resolveAuthorizationUrl(parameters: URLSearchParams, transport?: AndcoTransportMode): Promise<Result<URL>>;
|
|
116
|
+
/** Exchanges a refresh token. The caller decides where the rotated token is written. */
|
|
117
|
+
refresh(refreshToken: string): Promise<Result<AndcoSession>>;
|
|
118
|
+
/** Reads the authenticated subject for one access token. */
|
|
119
|
+
userInfo(accessToken: string): Promise<Result<AndcoUser>>;
|
|
120
|
+
/** Starts RFC 8628 Device Authorization for a runtime that cannot present a browser. */
|
|
121
|
+
createDeviceAuthorizationRequest(options?: Pick<AndcoAuthorizationOptions, "scopes" | "authorizations">): Promise<Result<AndcoDeviceAuthorization>>;
|
|
122
|
+
/**
|
|
123
|
+
* Waits for the user to approve a device authorization, then returns the session.
|
|
124
|
+
*
|
|
125
|
+
* The whole poll is one await: interval pacing, `authorization_pending`, and `slow_down` are
|
|
126
|
+
* handled inside. The Andco CLI currently writes that loop itself, including its own backoff and
|
|
127
|
+
* its own expiry arithmetic, which is a protocol detail an integrator should never have to know.
|
|
128
|
+
*/
|
|
129
|
+
awaitDeviceAuthorization(device: AndcoDeviceAuthorization, options?: {
|
|
130
|
+
signal?: AbortSignal;
|
|
131
|
+
}): Promise<Result<AndcoSession>>;
|
|
132
|
+
/**
|
|
133
|
+
* The underlying `openid-client` configuration.
|
|
134
|
+
*
|
|
135
|
+
* Exposed because `openid-client/passport` takes one directly. Without it the Passport package
|
|
136
|
+
* would have to build a second configuration from the same endpoints, which is how two
|
|
137
|
+
* descriptions of one Authorization Server came to exist. Synchronous, so a Passport strategy can
|
|
138
|
+
* be registered without awaiting anything.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```ts
|
|
142
|
+
* const { token_endpoint } = andco.oauth.configuration().serverMetadata();
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
configuration(): oidc.Configuration;
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=oauth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"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"}
|
package/dist/oauth.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import * as oidc from "openid-client";
|
|
2
|
+
import { ANDCO_ERROR_CODES, AndcoError, Result } from "./errors.js";
|
|
3
|
+
import { andcoServerMetadata } from "./server-metadata.generated.js";
|
|
4
|
+
/** Rejects a transaction whose callback arrived far too late to be genuine. */
|
|
5
|
+
const TRANSACTION_MAX_AGE_MS = 10 * 60 * 1_000;
|
|
6
|
+
/** Above this length a direct authorization request risks truncation, so PAR is used. */
|
|
7
|
+
const DIRECT_URL_LIMIT = 2_048;
|
|
8
|
+
/** Parameters whose presence alone selects a Pushed Authorization Request. */
|
|
9
|
+
const SENSITIVE_PARAMETERS = ["authorization_details", "login_hint"];
|
|
10
|
+
/**
|
|
11
|
+
* The one OAuth implementation in the SDK.
|
|
12
|
+
*
|
|
13
|
+
* It replaces four parallel ones: the hand-rolled browser client, the separate confidential client,
|
|
14
|
+
* and the flows written by hand inside the Better Auth and Passport packages. Those diverged, which
|
|
15
|
+
* is exactly the failure a protocol implementation must not have. The work is delegated to
|
|
16
|
+
* `openid-client`, which carries no Node built-ins and therefore runs in browsers, Expo, and Tauri.
|
|
17
|
+
*
|
|
18
|
+
* Nothing here holds a session. A request is created, handed to whatever presents it, and exchanged
|
|
19
|
+
* with the transaction the caller kept.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const { data: request } = await andco.oauth.createAuthorizationRequest({ scopes: ["email"] });
|
|
24
|
+
* const callbackUrl = await presentSomehow(request.authorizationUrl);
|
|
25
|
+
* const { data: session } = await andco.oauth.exchangeCallback({ callbackUrl, request });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export class AndcoOAuth {
|
|
29
|
+
#config;
|
|
30
|
+
#fetch;
|
|
31
|
+
#clientSecret;
|
|
32
|
+
#transport;
|
|
33
|
+
#configuration;
|
|
34
|
+
constructor(options) {
|
|
35
|
+
this.#config = options.config;
|
|
36
|
+
this.#fetch = options.fetch;
|
|
37
|
+
this.#clientSecret = options.clientSecret ?? null;
|
|
38
|
+
this.#transport = options.transport ?? "auto";
|
|
39
|
+
}
|
|
40
|
+
/** Whether this client authenticates itself with a secret. */
|
|
41
|
+
get isConfidential() {
|
|
42
|
+
return this.#clientSecret !== null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Prepares one Authorization Code transaction with S256 PKCE, and resolves the Authorization
|
|
46
|
+
* Transport Policy internally so a caller never chooses between a direct request and PAR.
|
|
47
|
+
*/
|
|
48
|
+
async createAuthorizationRequest(options = {}) {
|
|
49
|
+
try {
|
|
50
|
+
const redirectTo = this.#redirectTo(options.redirectTo);
|
|
51
|
+
const codeVerifier = oidc.randomPKCECodeVerifier();
|
|
52
|
+
const codeChallenge = await oidc.calculatePKCECodeChallenge(codeVerifier);
|
|
53
|
+
const state = options.state ?? oidc.randomState();
|
|
54
|
+
const parameters = this.#authorizationParameters(options, redirectTo, state, codeChallenge);
|
|
55
|
+
const authorizationUrl = await this.#resolveAuthorizationUrl(parameters);
|
|
56
|
+
return Result.ok({ authorizationUrl, codeVerifier, state, redirectTo, createdAt: Date.now() });
|
|
57
|
+
}
|
|
58
|
+
catch (cause) {
|
|
59
|
+
return Result.fail(AndcoError.from(cause, "authorization_request_failed"));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Validates a complete callback URL against the transaction that produced it, then exchanges the
|
|
64
|
+
* code. Validation happens here rather than in each presenter so every runtime — browser, Host
|
|
65
|
+
* bridge, terminal paste — gets the same checks.
|
|
66
|
+
*/
|
|
67
|
+
async exchangeCallback(options) {
|
|
68
|
+
try {
|
|
69
|
+
const callbackUrl = new URL(options.callbackUrl);
|
|
70
|
+
const { request } = options;
|
|
71
|
+
if (!Number.isSafeInteger(request.createdAt) || Date.now() - request.createdAt > TRANSACTION_MAX_AGE_MS) {
|
|
72
|
+
return Result.fail(ANDCO_ERROR_CODES.INVALID_CALLBACK, { message: "the authorization transaction expired" });
|
|
73
|
+
}
|
|
74
|
+
else if (!callbackMatchesRedirect(callbackUrl, new URL(request.redirectTo))) {
|
|
75
|
+
return Result.fail(ANDCO_ERROR_CODES.INVALID_CALLBACK, { message: "the callback does not match redirectTo" });
|
|
76
|
+
}
|
|
77
|
+
else if (callbackUrl.searchParams.get("state") !== request.state) {
|
|
78
|
+
return Result.fail(ANDCO_ERROR_CODES.INVALID_CALLBACK, { message: "the callback state does not correlate" });
|
|
79
|
+
}
|
|
80
|
+
const oauthError = callbackUrl.searchParams.get("error");
|
|
81
|
+
if (oauthError) {
|
|
82
|
+
return Result.fail(oauthError.slice(0, 256), {
|
|
83
|
+
message: callbackUrl.searchParams.get("error_description")?.slice(0, 2_048) ?? oauthError,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
else if (!callbackUrl.searchParams.has("code")) {
|
|
87
|
+
return Result.fail(ANDCO_ERROR_CODES.INVALID_CALLBACK, {
|
|
88
|
+
message: "the callback carries no authorization code",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const tokens = await oidc.authorizationCodeGrant(this.#oidc(), callbackUrl, {
|
|
92
|
+
pkceCodeVerifier: request.codeVerifier,
|
|
93
|
+
expectedState: request.state,
|
|
94
|
+
});
|
|
95
|
+
return await this.#sessionFrom(tokens);
|
|
96
|
+
}
|
|
97
|
+
catch (cause) {
|
|
98
|
+
return Result.fail(AndcoError.from(cause, "authorization_exchange_failed"));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Resolves an authorization request someone else already assembled.
|
|
103
|
+
*
|
|
104
|
+
* Exists for integrations that generate their own OAuth request — Better Auth and Passport both
|
|
105
|
+
* do — and still need the Authorization Transport Policy applied. Without it each one reimplements
|
|
106
|
+
* the direct-versus-PAR decision, which is how three implementations of it came to exist.
|
|
107
|
+
*
|
|
108
|
+
* `transport` overrides the instance's own policy for this call only — Passport exposes this as a
|
|
109
|
+
* per-strategy default, distinct from the client-wide policy every other caller gets.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```ts
|
|
113
|
+
* const { data: url } = await andco.oauth.resolveAuthorizationUrl(parametersFromBetterAuth);
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
async resolveAuthorizationUrl(parameters, transport) {
|
|
117
|
+
try {
|
|
118
|
+
return Result.ok(await this.#resolveAuthorizationUrl(parameters, transport));
|
|
119
|
+
}
|
|
120
|
+
catch (cause) {
|
|
121
|
+
return Result.fail(AndcoError.from(cause, "authorization_request_failed"));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** Exchanges a refresh token. The caller decides where the rotated token is written. */
|
|
125
|
+
async refresh(refreshToken) {
|
|
126
|
+
try {
|
|
127
|
+
const tokens = await oidc.refreshTokenGrant(this.#oidc(), refreshToken);
|
|
128
|
+
return await this.#sessionFrom(tokens);
|
|
129
|
+
}
|
|
130
|
+
catch (cause) {
|
|
131
|
+
return Result.fail(AndcoError.from(cause, "refresh_failed"));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Reads the authenticated subject for one access token. */
|
|
135
|
+
async userInfo(accessToken) {
|
|
136
|
+
try {
|
|
137
|
+
// The subject check compares the UserInfo response against the token's own `sub`, which
|
|
138
|
+
// exists only when the token is a JWT. An opaque token offers nothing to compare, so the
|
|
139
|
+
// check is skipped rather than failing a request that is otherwise valid.
|
|
140
|
+
const subject = decodeSubject(accessToken);
|
|
141
|
+
const info = await oidc.fetchUserInfo(this.#oidc(), accessToken, subject ? subject.sub : oidc.skipSubjectCheck);
|
|
142
|
+
return Result.ok({
|
|
143
|
+
id: String(info.sub),
|
|
144
|
+
name: info.name,
|
|
145
|
+
email: info.email,
|
|
146
|
+
avatarUrl: info.picture,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch (cause) {
|
|
150
|
+
return Result.fail(AndcoError.from(cause, "userinfo_failed"));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Starts RFC 8628 Device Authorization for a runtime that cannot present a browser. */
|
|
154
|
+
async createDeviceAuthorizationRequest(options = {}) {
|
|
155
|
+
try {
|
|
156
|
+
const response = await oidc.initiateDeviceAuthorization(this.#oidc(), {
|
|
157
|
+
scope: this.#scopes(options).join(" "),
|
|
158
|
+
});
|
|
159
|
+
return Result.ok({
|
|
160
|
+
deviceCode: response.device_code,
|
|
161
|
+
userCode: response.user_code,
|
|
162
|
+
verificationUri: response.verification_uri,
|
|
163
|
+
verificationUriComplete: response.verification_uri_complete ?? response.verification_uri,
|
|
164
|
+
expiresIn: response.expires_in,
|
|
165
|
+
interval: response.interval ?? 5,
|
|
166
|
+
raw: response,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch (cause) {
|
|
170
|
+
return Result.fail(AndcoError.from(cause, "device_authorization_failed"));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Waits for the user to approve a device authorization, then returns the session.
|
|
175
|
+
*
|
|
176
|
+
* The whole poll is one await: interval pacing, `authorization_pending`, and `slow_down` are
|
|
177
|
+
* handled inside. The Andco CLI currently writes that loop itself, including its own backoff and
|
|
178
|
+
* its own expiry arithmetic, which is a protocol detail an integrator should never have to know.
|
|
179
|
+
*/
|
|
180
|
+
async awaitDeviceAuthorization(device, options = {}) {
|
|
181
|
+
try {
|
|
182
|
+
const tokens = await oidc.pollDeviceAuthorizationGrant(this.#oidc(), device.raw, undefined, {
|
|
183
|
+
signal: options.signal,
|
|
184
|
+
});
|
|
185
|
+
return await this.#sessionFrom(tokens);
|
|
186
|
+
}
|
|
187
|
+
catch (cause) {
|
|
188
|
+
return Result.fail(AndcoError.from(cause, "device_exchange_failed"));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* The underlying `openid-client` configuration.
|
|
193
|
+
*
|
|
194
|
+
* Exposed because `openid-client/passport` takes one directly. Without it the Passport package
|
|
195
|
+
* would have to build a second configuration from the same endpoints, which is how two
|
|
196
|
+
* descriptions of one Authorization Server came to exist. Synchronous, so a Passport strategy can
|
|
197
|
+
* be registered without awaiting anything.
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```ts
|
|
201
|
+
* const { token_endpoint } = andco.oauth.configuration().serverMetadata();
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
configuration() {
|
|
205
|
+
return this.#oidc();
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* The Authorization Server, as the Authorization Server describes itself.
|
|
209
|
+
*
|
|
210
|
+
* The description is real — it comes from the server's own discovery document — but it is read
|
|
211
|
+
* at build time by `scripts/refresh-server-metadata.mjs` rather than at runtime by every
|
|
212
|
+
* application. ADR-0018 records what hand-written guesses cost; ADR-0021 records why the reading
|
|
213
|
+
* moved to the build. The ceiling is drift: a server that changes an endpoint or an algorithm
|
|
214
|
+
* breaks clients built before the change, and only regenerating the file fixes them.
|
|
215
|
+
*/
|
|
216
|
+
#oidc() {
|
|
217
|
+
this.#configuration ??= this.#build();
|
|
218
|
+
return this.#configuration;
|
|
219
|
+
}
|
|
220
|
+
#build() {
|
|
221
|
+
const config = this.#config;
|
|
222
|
+
const base = config.endpoints.auth;
|
|
223
|
+
// `issuer` must match the `iss` the server signs, which carries no trailing slash.
|
|
224
|
+
const issuer = `${base.origin}${base.pathname.replace(/\/+$/, "")}`;
|
|
225
|
+
const secret = this.#clientSecret;
|
|
226
|
+
const configuration = new oidc.Configuration(andcoServerMetadata(issuer), config.clientId, secret ?? undefined, secret ? oidc.ClientSecretBasic(secret) : oidc.None());
|
|
227
|
+
// `CustomFetch` widens the body to include `Uint8Array`, which every runtime's `fetch` accepts
|
|
228
|
+
// as a `BufferSource` even though the standard lib types do not spell it that way.
|
|
229
|
+
configuration[oidc.customFetch] = ((input, init) => this.#fetch(input, init));
|
|
230
|
+
// The Andco Authorization Server is reachable over loopback HTTP in local development.
|
|
231
|
+
oidc.allowInsecureRequests(configuration);
|
|
232
|
+
return configuration;
|
|
233
|
+
}
|
|
234
|
+
#redirectTo(override) {
|
|
235
|
+
const value = override ?? this.#config.redirectTo;
|
|
236
|
+
if (!value) {
|
|
237
|
+
throw new AndcoError(ANDCO_ERROR_CODES.INVALID_CONFIGURATION, {
|
|
238
|
+
message: "redirectTo is required, on the instance or on the request",
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return new URL(value).href;
|
|
242
|
+
}
|
|
243
|
+
#scopes(options) {
|
|
244
|
+
const contributed = (options.authorizations ?? []).flatMap((a) => [...a.scopes]);
|
|
245
|
+
const requested = options.scopes ?? this.#config.initialScopes;
|
|
246
|
+
return [...new Set([...requested, ...contributed])];
|
|
247
|
+
}
|
|
248
|
+
#authorizationParameters(options, redirectTo, state, codeChallenge) {
|
|
249
|
+
const parameters = new URLSearchParams({
|
|
250
|
+
client_id: this.#config.clientId,
|
|
251
|
+
response_type: "code",
|
|
252
|
+
redirect_uri: redirectTo,
|
|
253
|
+
state,
|
|
254
|
+
code_challenge: codeChallenge,
|
|
255
|
+
code_challenge_method: "S256",
|
|
256
|
+
});
|
|
257
|
+
const scopes = this.#scopes(options);
|
|
258
|
+
if (scopes.length)
|
|
259
|
+
parameters.set("scope", scopes.join(" "));
|
|
260
|
+
const resources = new Set();
|
|
261
|
+
for (const value of toArray(options.resource))
|
|
262
|
+
resources.add(value);
|
|
263
|
+
for (const contribution of options.authorizations ?? [])
|
|
264
|
+
resources.add(contribution.resource);
|
|
265
|
+
for (const resource of resources)
|
|
266
|
+
parameters.append("resource", resource);
|
|
267
|
+
const details = [
|
|
268
|
+
...(options.authorizationDetails ?? []),
|
|
269
|
+
...(options.authorizations ?? []).flatMap((a) => [...(a.authorizationDetails ?? [])]),
|
|
270
|
+
];
|
|
271
|
+
if (details.length)
|
|
272
|
+
parameters.set("authorization_details", JSON.stringify(details));
|
|
273
|
+
const orgId = options.orgId ?? (options.authorizations ?? []).find((a) => a.orgId)?.orgId;
|
|
274
|
+
if (orgId)
|
|
275
|
+
parameters.set("org_id", orgId);
|
|
276
|
+
if (options.externalId)
|
|
277
|
+
parameters.set("external_id", options.externalId);
|
|
278
|
+
return parameters;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Chooses between a direct request and a Pushed Authorization Request.
|
|
282
|
+
*
|
|
283
|
+
* `auto` pushes whenever the request carries RFC 9396 details or would otherwise be long enough
|
|
284
|
+
* to risk truncation, and sends a direct request otherwise. Nothing falls back silently: a failed
|
|
285
|
+
* push is reported rather than downgraded, because downgrading would move sensitive parameters
|
|
286
|
+
* into a URL the user agent logs.
|
|
287
|
+
*/
|
|
288
|
+
async #resolveAuthorizationUrl(parameters, transportOverride) {
|
|
289
|
+
const configuration = this.#oidc();
|
|
290
|
+
const direct = oidc.buildAuthorizationUrl(configuration, parameters);
|
|
291
|
+
// A user identifier and fine-grained permission details must not travel in a URL the user
|
|
292
|
+
// agent records in history, sends as a Referer, or writes to a log.
|
|
293
|
+
const sensitive = SENSITIVE_PARAMETERS.some((name) => parameters.has(name));
|
|
294
|
+
const transport = transportOverride ?? this.#transport;
|
|
295
|
+
const mode = transport === "auto" ? (sensitive || direct.href.length > DIRECT_URL_LIMIT ? "par" : "get") : transport;
|
|
296
|
+
if (mode === "get")
|
|
297
|
+
return direct;
|
|
298
|
+
return await oidc.buildAuthorizationUrlWithPAR(configuration, parameters);
|
|
299
|
+
}
|
|
300
|
+
async #sessionFrom(tokens) {
|
|
301
|
+
if (tokens.token_type && tokens.token_type.toLowerCase() !== "bearer") {
|
|
302
|
+
return Result.fail(ANDCO_ERROR_CODES.INVALID_RESPONSE, { message: "unsupported token type" });
|
|
303
|
+
}
|
|
304
|
+
// The ID Token is where OpenID Connect puts the subject. Reading it out of the access token
|
|
305
|
+
// works only while that token happens to be a JWT, which no specification promises and an
|
|
306
|
+
// Authorization Server may stop doing without warning.
|
|
307
|
+
const subject = tokens.claims?.()?.sub ?? decodeSubject(tokens.access_token)?.sub;
|
|
308
|
+
if (!subject)
|
|
309
|
+
return Result.fail(ANDCO_ERROR_CODES.INVALID_RESPONSE, { message: "the response carries no subject" });
|
|
310
|
+
const session = {
|
|
311
|
+
accessToken: tokens.access_token,
|
|
312
|
+
refreshToken: tokens.refresh_token ?? null,
|
|
313
|
+
tokenType: tokens.token_type ?? "bearer",
|
|
314
|
+
expiresAt: Math.floor(Date.now() / 1000) + (tokens.expires_in ?? 0),
|
|
315
|
+
scopes: (tokens.scope ?? "").split(/\s+/).filter(Boolean),
|
|
316
|
+
user: { id: subject, name: null, email: null, avatarUrl: null },
|
|
317
|
+
};
|
|
318
|
+
const user = await this.userInfo(session.accessToken);
|
|
319
|
+
return Result.ok(user.data ? { ...session, user: user.data } : session);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function toArray(value) {
|
|
323
|
+
if (value === undefined)
|
|
324
|
+
return [];
|
|
325
|
+
return typeof value === "string" ? [value] : value;
|
|
326
|
+
}
|
|
327
|
+
/** A callback must reach the exact registered redirect, not merely a similar one. */
|
|
328
|
+
function callbackMatchesRedirect(actual, expected) {
|
|
329
|
+
if (actual.origin !== expected.origin || actual.pathname !== expected.pathname)
|
|
330
|
+
return false;
|
|
331
|
+
return [...expected.searchParams].every(([key, value]) => actual.searchParams.getAll(key).includes(value));
|
|
332
|
+
}
|
|
333
|
+
/** Reads `sub` from an access token without verifying it; the server remains the authority. */
|
|
334
|
+
function decodeSubject(accessToken) {
|
|
335
|
+
const segments = accessToken.split(".");
|
|
336
|
+
if (segments.length < 2 || !segments[1])
|
|
337
|
+
return null;
|
|
338
|
+
try {
|
|
339
|
+
const payload = JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(segments[1].replace(/-/g, "+").replace(/_/g, "/")), (character) => character.charCodeAt(0))));
|
|
340
|
+
if (payload && typeof payload === "object" && typeof payload.sub === "string") {
|
|
341
|
+
return { sub: payload.sub };
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Result } from "./errors.js";
|
|
2
|
+
/** How an authorization is put in front of the user. */
|
|
3
|
+
export type AndcoPresentation = "popup" | "redirect";
|
|
4
|
+
export type AndcoPresentOptions = {
|
|
5
|
+
url: URL;
|
|
6
|
+
presentation: AndcoPresentation;
|
|
7
|
+
/** Exact callback the Authorization Server will reach on success. */
|
|
8
|
+
returnTo: URL;
|
|
9
|
+
/** Exact callback reached on failure. Defaults to `returnTo`. */
|
|
10
|
+
errorReturnTo?: URL;
|
|
11
|
+
/** Correlates this presentation independently of OAuth state. */
|
|
12
|
+
presentationId: string;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The boundary between the protocol and whatever shows it to a person.
|
|
17
|
+
*
|
|
18
|
+
* A presenter receives an authorization URL and answers with the callback URL that came back, or
|
|
19
|
+
* `null` when the user dismissed. It owns no protocol: no PKCE, no state, no token exchange.
|
|
20
|
+
*
|
|
21
|
+
* Keeping it this narrow is what makes sign-in testable without a browser — a test supplies a
|
|
22
|
+
* presenter that returns a prepared callback URL and the whole flow runs in plain Node. It is also
|
|
23
|
+
* why a redirect presentation can answer with `null` and never resolve: the document is replaced,
|
|
24
|
+
* and the result arrives on the next page load instead.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* const presenter: AndcoPresenter = {
|
|
29
|
+
* present: async ({ url }) => Result.ok(new URL(`${redirectTo}?code=test&state=${state}`)),
|
|
30
|
+
* };
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export interface AndcoPresenter {
|
|
34
|
+
present(options: AndcoPresentOptions): Promise<Result<URL | null>>;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=presenter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presenter.d.ts","sourceRoot":"","sources":["../src/presenter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,wDAAwD;AACxD,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,UAAU,CAAC;AAErD,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,EAAE,GAAG,CAAC;IACT,YAAY,EAAE,iBAAiB,CAAC;IAChC,qEAAqE;IACrE,QAAQ,EAAE,GAAG,CAAC;IACd,iEAAiE;IACjE,aAAa,CAAC,EAAE,GAAG,CAAC;IACpB,iEAAiE;IACjE,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC;CACpE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/rest.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type ResultClientFor } from "@andco/openapi-fetch";
|
|
2
|
+
import { type AndCoRestClient } from "@andco/protocol/transport";
|
|
3
|
+
import type { AndcoConfig } from "./config.js";
|
|
4
|
+
import type { AndcoCredentials } from "./credentials.js";
|
|
5
|
+
import { AndcoAPIError } from "./errors.js";
|
|
6
|
+
export type AndcoRestOptions = {
|
|
7
|
+
config: AndcoConfig;
|
|
8
|
+
fetch: typeof globalThis.fetch;
|
|
9
|
+
credentials: AndcoCredentials;
|
|
10
|
+
/** Resource Indicator whose token authorizes these requests. Defaults to the configured API. */
|
|
11
|
+
resource?: string;
|
|
12
|
+
};
|
|
13
|
+
/** One cursor page of a server-paginated list. `after` resumes from a previous `next_cursor`. */
|
|
14
|
+
export type AndcoPageOptions = {
|
|
15
|
+
after?: string;
|
|
16
|
+
limit?: number;
|
|
17
|
+
};
|
|
18
|
+
export type AndcoRequestOptions = {
|
|
19
|
+
/** Query parameters. Array values repeat the key, which is what the Andco API expects. */
|
|
20
|
+
query?: Record<string, string | number | boolean | readonly string[] | undefined>;
|
|
21
|
+
headers?: Record<string, string>;
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
/** Makes a write idempotent. One is generated when omitted. */
|
|
24
|
+
idempotencyKey?: string;
|
|
25
|
+
};
|
|
26
|
+
/** `AndcoRest.http`'s type: the generated OAuth resource client, with `AndcoAPIError` as its error. */
|
|
27
|
+
export type AndcoHttpClient = ResultClientFor<AndCoRestClient, AndcoAPIError>;
|
|
28
|
+
/**
|
|
29
|
+
* Typed access to the Andco resource API.
|
|
30
|
+
*
|
|
31
|
+
* `http` never throws by default — every call resolves `{ data, error }` — and gains
|
|
32
|
+
* `.throwOnError()` for a caller that would rather reject. The transport resolves a token per
|
|
33
|
+
* request through the bound credentials, so a stale token refreshes without the caller knowing.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* const { data, error } = await bound.rest.http.GET("/accounts");
|
|
38
|
+
* if (error) return reportUnavailable(error.code);
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare class AndcoRest {
|
|
42
|
+
#private;
|
|
43
|
+
/**
|
|
44
|
+
* Typed OAuth resource client generated from the API's OpenAPI contract.
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* const { data, error } = await bound.rest.http.GET("/accounts");
|
|
48
|
+
* if (error) return reportUnavailable(error.code); // error is `AndcoError`
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
readonly http: AndcoHttpClient;
|
|
52
|
+
/**
|
|
53
|
+
* The raw OAuth resource client generated from the API's OpenAPI contract. Use this for low-level access to the API endpoints.
|
|
54
|
+
*/
|
|
55
|
+
readonly raw: AndCoRestClient;
|
|
56
|
+
constructor(options: AndcoRestOptions);
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=rest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rest.d.ts","sourceRoot":"","sources":["../src/rest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAc,MAAM,sBAAsB,CAAC;AACxE,OAAO,EAAE,KAAK,eAAe,EAAyB,MAAM,2BAA2B,CAAC;AACxF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAC/B,WAAW,EAAE,gBAAgB,CAAC;IAC9B,gGAAgG;IAChG,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,iGAAiG;AACjG,MAAM,MAAM,gBAAgB,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAElE,MAAM,MAAM,mBAAmB,GAAG;IAChC,0FAA0F;IAC1F,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IAClF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,+DAA+D;IAC/D,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,uGAAuG;AACvG,MAAM,MAAM,eAAe,GAAG,eAAe,CAAC,eAAe,EAAE,aAAa,CAAC,CAAC;AAE9E;;;;;;;;;;;;GAYG;AACH,qBAAa,SAAS;;IAMpB;;;;;;;OAOG;IACH,SAAgB,IAAI,EAAE,eAAe,CAAC;IAEtC;;OAEG;IACH,SAAgB,GAAG,EAAE,eAAe,CAAC;gBAElB,OAAO,EAAE,gBAAgB;CAc7C"}
|
package/dist/rest.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { withResult } from "@andco/openapi-fetch";
|
|
2
|
+
import { createAndCoRestClient } from "@andco/protocol/transport";
|
|
3
|
+
import { AndcoAPIError } from "./errors.js";
|
|
4
|
+
/**
|
|
5
|
+
* Typed access to the Andco resource API.
|
|
6
|
+
*
|
|
7
|
+
* `http` never throws by default — every call resolves `{ data, error }` — and gains
|
|
8
|
+
* `.throwOnError()` for a caller that would rather reject. The transport resolves a token per
|
|
9
|
+
* request through the bound credentials, so a stale token refreshes without the caller knowing.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const { data, error } = await bound.rest.http.GET("/accounts");
|
|
14
|
+
* if (error) return reportUnavailable(error.code);
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export class AndcoRest {
|
|
18
|
+
#config;
|
|
19
|
+
#fetch;
|
|
20
|
+
#credentials;
|
|
21
|
+
#resource;
|
|
22
|
+
/**
|
|
23
|
+
* Typed OAuth resource client generated from the API's OpenAPI contract.
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const { data, error } = await bound.rest.http.GET("/accounts");
|
|
27
|
+
* if (error) return reportUnavailable(error.code); // error is `AndcoError`
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
http;
|
|
31
|
+
/**
|
|
32
|
+
* The raw OAuth resource client generated from the API's OpenAPI contract. Use this for low-level access to the API endpoints.
|
|
33
|
+
*/
|
|
34
|
+
raw;
|
|
35
|
+
constructor(options) {
|
|
36
|
+
this.#config = options.config;
|
|
37
|
+
this.#fetch = options.fetch;
|
|
38
|
+
this.#credentials = options.credentials;
|
|
39
|
+
this.#resource = options.resource ?? options.config.endpoints.api.origin;
|
|
40
|
+
this.raw = createAndCoRestClient({
|
|
41
|
+
endpoint: this.#config.endpoints.api,
|
|
42
|
+
apiVersion: this.#config.apiVersion,
|
|
43
|
+
accessToken: () => this.#credentials.accessTokenFor(this.#resource),
|
|
44
|
+
fetch: this.#fetch,
|
|
45
|
+
});
|
|
46
|
+
this.http = withResult(this.raw, AndcoAPIError.fromOpenAPIFetch);
|
|
47
|
+
}
|
|
48
|
+
}
|