@andco/sdk 0.0.2 → 0.0.4
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 +1 -1
- package/README.md +96 -54
- package/dist/auth.d.ts +8 -3
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +32 -23
- package/dist/browser/controller.d.ts +55 -1
- package/dist/browser/controller.d.ts.map +1 -1
- package/dist/browser/controller.js +166 -9
- package/dist/browser/frame.d.ts +4 -3
- package/dist/browser/frame.d.ts.map +1 -1
- package/dist/browser/frame.js +12 -20
- package/dist/browser/index.d.ts +18 -6
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +64 -29
- package/dist/browser/popup.d.ts +6 -1
- package/dist/browser/popup.d.ts.map +1 -1
- package/dist/browser/popup.js +37 -8
- package/dist/cli/index.d.ts +4 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +10 -3
- package/dist/cli/server.d.ts +5 -0
- package/dist/cli/server.d.ts.map +1 -1
- package/dist/cli/server.js +5 -0
- package/dist/client.d.ts +66 -19
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +125 -74
- package/dist/config.d.ts +2 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/credentials.d.ts +58 -4
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +0 -0
- package/dist/errors.d.ts +23 -21
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +18 -20
- package/dist/globals.d.ts +25 -0
- package/dist/globals.d.ts.map +1 -0
- package/dist/globals.js +15 -0
- package/dist/grants-api.d.ts +34 -0
- package/dist/grants-api.d.ts.map +1 -0
- package/dist/grants-api.js +48 -0
- package/dist/grants.d.ts +16 -0
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +13 -0
- package/dist/index.d.ts +12 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/inflight.d.ts +31 -0
- package/dist/inflight.d.ts.map +1 -0
- package/dist/inflight.js +26 -0
- package/dist/intents.d.ts +386 -40
- package/dist/intents.d.ts.map +1 -1
- package/dist/intents.js +712 -53
- package/dist/oauth.d.ts +55 -6
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +86 -57
- package/dist/presenter.d.ts +59 -11
- package/dist/presenter.d.ts.map +1 -1
- package/dist/presenter.js +40 -1
- package/dist/resource.d.ts +109 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/resource.js +151 -0
- package/dist/rest.d.ts +23 -4
- package/dist/rest.d.ts.map +1 -1
- package/dist/rest.js +46 -5
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server-metadata.generated.d.ts.map +1 -1
- package/dist/server-metadata.generated.js +8 -4
- package/dist/service.d.ts +109 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +241 -0
- package/dist/session-store.d.ts +12 -4
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +64 -14
- package/dist/storage.d.ts +16 -23
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +27 -25
- package/dist/tokens.d.ts +46 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +148 -0
- package/package.json +13 -3
package/dist/oauth.d.ts
CHANGED
|
@@ -2,28 +2,51 @@ import * as oidc from "openid-client";
|
|
|
2
2
|
import type { AndcoConfig, AndcoScope } from "./config.js";
|
|
3
3
|
import type { AndcoSession, AndcoUser } from "./credentials.js";
|
|
4
4
|
import { Result } from "./errors.js";
|
|
5
|
+
import type { AndcoGlobals } from "./globals.js";
|
|
5
6
|
/** An RFC 9396 authorization detail. Its shape is owned by the Resource Server that defines it. */
|
|
6
7
|
export type AndcoAuthorizationDetail = {
|
|
7
8
|
readonly type: string;
|
|
8
9
|
} & Readonly<Record<string, unknown>>;
|
|
9
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* One Resource Server's contribution to a single authorization request.
|
|
12
|
+
*
|
|
13
|
+
* Whose resources it asks for lives in each detail's `subject`, for Andco's types and third-party
|
|
14
|
+
* types alike: absent or `null` is the signed-in Profile, `{ type: "organization" }` lets the person
|
|
15
|
+
* pick one of their Organizations, and an `identifier` fixes it.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const contribution: AndcoResourceAuthorization = {
|
|
20
|
+
* resourceServerId: "00000000-0000-0000-0000-00000000f301",
|
|
21
|
+
* resource: "https://openfactura.localhost/api",
|
|
22
|
+
* scopes: [],
|
|
23
|
+
* authorizationDetails: [
|
|
24
|
+
* { type: "documents", actions: ["read"], subject: { type: "organization", identifier: "2" } },
|
|
25
|
+
* ],
|
|
26
|
+
* };
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
10
29
|
export type AndcoResourceAuthorization = {
|
|
30
|
+
/** Registered Resource Server ID carried by this contribution. */
|
|
11
31
|
readonly resourceServerId: string;
|
|
32
|
+
/** OAuth Resource Indicator; each distinct contribution becomes a repeated `resource` parameter. */
|
|
12
33
|
readonly resource: string;
|
|
13
34
|
readonly scopes: readonly AndcoScope[];
|
|
14
35
|
readonly authorizationDetails?: readonly AndcoAuthorizationDetail[];
|
|
15
|
-
readonly orgId?: string;
|
|
16
36
|
};
|
|
17
37
|
/** How an authorization request reaches the Authorization Server. */
|
|
18
38
|
export type AndcoTransportMode = "auto" | "get" | "par";
|
|
19
39
|
/** Everything a caller may vary for one authorization request. */
|
|
20
40
|
export type AndcoAuthorizationOptions = {
|
|
41
|
+
/** Registered callback URL. Defaults to the instance's `redirectTo`. */
|
|
21
42
|
redirectTo?: string | URL;
|
|
43
|
+
/** Requested scopes, merged with resource contributions. Defaults to `initialScopes`. */
|
|
22
44
|
scopes?: readonly AndcoScope[];
|
|
23
45
|
/** Immutable contributions from Resource Server Definitions, composed into one request. */
|
|
24
46
|
authorizations?: readonly AndcoResourceAuthorization[];
|
|
47
|
+
/** Token audiences, merged with resource contributions and deduplicated. */
|
|
25
48
|
resource?: string | readonly string[];
|
|
26
|
-
|
|
49
|
+
/** Fine-grained permissions merged with resource contributions; selects PAR in `auto` mode. */
|
|
27
50
|
authorizationDetails?: readonly AndcoAuthorizationDetail[];
|
|
28
51
|
/** Opaque destination reference scoped to this OAuth Client. */
|
|
29
52
|
externalId?: string;
|
|
@@ -36,10 +59,15 @@ export type AndcoAuthorizationOptions = {
|
|
|
36
59
|
* terminal's memory.
|
|
37
60
|
*/
|
|
38
61
|
export type AndcoAuthorizationRequest = {
|
|
62
|
+
/** URL to present to the user; may contain a short-lived PAR reference. */
|
|
39
63
|
authorizationUrl: URL;
|
|
64
|
+
/** Private PKCE verifier; persist with the transaction and never include it in authorization URLs or logs. */
|
|
40
65
|
codeVerifier: string;
|
|
66
|
+
/** Correlates the callback with this transaction. */
|
|
41
67
|
state: string;
|
|
68
|
+
/** Exact callback against which the response is validated. */
|
|
42
69
|
redirectTo: string;
|
|
70
|
+
/** Unix time in milliseconds; callbacks older than ten minutes are rejected. */
|
|
43
71
|
createdAt: number;
|
|
44
72
|
};
|
|
45
73
|
/** RFC 8628 instructions a constrained client displays instead of opening a browser. */
|
|
@@ -48,16 +76,21 @@ export type AndcoDeviceAuthorization = {
|
|
|
48
76
|
userCode: string;
|
|
49
77
|
verificationUri: string;
|
|
50
78
|
verificationUriComplete: string;
|
|
79
|
+
/** Lifetime of the device code, in seconds. */
|
|
51
80
|
expiresIn: number;
|
|
81
|
+
/** Minimum polling interval from the server, in seconds. */
|
|
52
82
|
interval: number;
|
|
53
83
|
/** The server response, kept so polling needs no reconstruction by the caller. */
|
|
54
84
|
raw: oidc.DeviceAuthorizationResponse;
|
|
55
85
|
};
|
|
86
|
+
/** OAuth endpoints, client authentication, transport policy, and runtime capabilities. */
|
|
56
87
|
export type AndcoOAuthOptions = {
|
|
57
88
|
config: AndcoConfig;
|
|
58
|
-
fetch
|
|
89
|
+
/** Platform capabilities; only `fetch` is read today. `resolveGlobals()` builds one. */
|
|
90
|
+
globals: AndcoGlobals;
|
|
59
91
|
/** Present for a confidential client, absent for a public one. */
|
|
60
92
|
clientSecret?: string | null;
|
|
93
|
+
/** Defaults to `auto`: PAR for sensitive parameters or URLs over 2048 characters, otherwise GET. */
|
|
61
94
|
transport?: AndcoTransportMode;
|
|
62
95
|
};
|
|
63
96
|
/**
|
|
@@ -117,8 +150,24 @@ export declare class AndcoOAuth {
|
|
|
117
150
|
refresh(refreshToken: string): Promise<Result<AndcoSession>>;
|
|
118
151
|
/** Reads the authenticated subject for one access token. */
|
|
119
152
|
userInfo(accessToken: string): Promise<Result<AndcoUser>>;
|
|
120
|
-
/**
|
|
121
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Starts RFC 8628 Device Authorization for a runtime that cannot present a browser.
|
|
155
|
+
*
|
|
156
|
+
* Takes the same permissions as {@link createAuthorizationRequest}: scopes, Resource Server
|
|
157
|
+
* contributions (bound to their server through the same plan, with one `resource` parameter
|
|
158
|
+
* each) and loose `authorizationDetails`. They travel in the device request's form body, which
|
|
159
|
+
* the Authorization Server records exactly as it records a pushed request.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```ts
|
|
163
|
+
* const { data: device } = await andco.oauth.createDeviceAuthorizationRequest({
|
|
164
|
+
* scopes: ["openid", "email"],
|
|
165
|
+
* authorizations: [bank.accounts.authorization({ actions: ["read"], subject: { type: "profile" } })],
|
|
166
|
+
* });
|
|
167
|
+
* console.log(device.verificationUriComplete, device.userCode);
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
createDeviceAuthorizationRequest(options?: Pick<AndcoAuthorizationOptions, "scopes" | "authorizations" | "authorizationDetails" | "resource">): Promise<Result<AndcoDeviceAuthorization>>;
|
|
122
171
|
/**
|
|
123
172
|
* Waits for the user to approve a device authorization, then returns the session.
|
|
124
173
|
*
|
package/dist/oauth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"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"}
|
package/dist/oauth.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createAndCoAuthorizationPlan, decodeJwtPayload } from "@andco/protocol";
|
|
1
2
|
import * as oidc from "openid-client";
|
|
2
3
|
import { ANDCO_ERROR_CODES, AndcoError, Result } from "./errors.js";
|
|
3
4
|
import { andcoServerMetadata } from "./server-metadata.generated.js";
|
|
@@ -27,13 +28,13 @@ const SENSITIVE_PARAMETERS = ["authorization_details", "login_hint"];
|
|
|
27
28
|
*/
|
|
28
29
|
export class AndcoOAuth {
|
|
29
30
|
#config;
|
|
30
|
-
#
|
|
31
|
+
#globals;
|
|
31
32
|
#clientSecret;
|
|
32
33
|
#transport;
|
|
33
34
|
#configuration;
|
|
34
35
|
constructor(options) {
|
|
35
36
|
this.#config = options.config;
|
|
36
|
-
this.#
|
|
37
|
+
this.#globals = options.globals;
|
|
37
38
|
this.#clientSecret = options.clientSecret ?? null;
|
|
38
39
|
this.#transport = options.transport ?? "auto";
|
|
39
40
|
}
|
|
@@ -56,7 +57,7 @@ export class AndcoOAuth {
|
|
|
56
57
|
return Result.ok({ authorizationUrl, codeVerifier, state, redirectTo, createdAt: Date.now() });
|
|
57
58
|
}
|
|
58
59
|
catch (cause) {
|
|
59
|
-
return Result.fail(AndcoError.from(cause,
|
|
60
|
+
return Result.fail(AndcoError.from(cause, ANDCO_ERROR_CODES.AUTHORIZATION_REQUEST_FAILED));
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
/**
|
|
@@ -79,9 +80,12 @@ export class AndcoOAuth {
|
|
|
79
80
|
}
|
|
80
81
|
const oauthError = callbackUrl.searchParams.get("error");
|
|
81
82
|
if (oauthError) {
|
|
82
|
-
|
|
83
|
+
// The Authorization Server's own RFC 6749 `error` parameter — a foreign vocabulary, not one
|
|
84
|
+
// of the SDK's own codes, so it is built through the open `AndcoError` constructor rather
|
|
85
|
+
// than `Result.fail`'s closed shorthand.
|
|
86
|
+
return Result.fail(new AndcoError(oauthError.slice(0, 256), {
|
|
83
87
|
message: callbackUrl.searchParams.get("error_description")?.slice(0, 2_048) ?? oauthError,
|
|
84
|
-
});
|
|
88
|
+
}));
|
|
85
89
|
}
|
|
86
90
|
else if (!callbackUrl.searchParams.has("code")) {
|
|
87
91
|
return Result.fail(ANDCO_ERROR_CODES.INVALID_CALLBACK, {
|
|
@@ -95,7 +99,7 @@ export class AndcoOAuth {
|
|
|
95
99
|
return await this.#sessionFrom(tokens);
|
|
96
100
|
}
|
|
97
101
|
catch (cause) {
|
|
98
|
-
return Result.fail(AndcoError.from(cause,
|
|
102
|
+
return Result.fail(AndcoError.from(cause, ANDCO_ERROR_CODES.AUTHORIZATION_EXCHANGE_FAILED));
|
|
99
103
|
}
|
|
100
104
|
}
|
|
101
105
|
/**
|
|
@@ -118,7 +122,7 @@ export class AndcoOAuth {
|
|
|
118
122
|
return Result.ok(await this.#resolveAuthorizationUrl(parameters, transport));
|
|
119
123
|
}
|
|
120
124
|
catch (cause) {
|
|
121
|
-
return Result.fail(AndcoError.from(cause,
|
|
125
|
+
return Result.fail(AndcoError.from(cause, ANDCO_ERROR_CODES.AUTHORIZATION_REQUEST_FAILED));
|
|
122
126
|
}
|
|
123
127
|
}
|
|
124
128
|
/** Exchanges a refresh token. The caller decides where the rotated token is written. */
|
|
@@ -128,7 +132,7 @@ export class AndcoOAuth {
|
|
|
128
132
|
return await this.#sessionFrom(tokens);
|
|
129
133
|
}
|
|
130
134
|
catch (cause) {
|
|
131
|
-
return Result.fail(AndcoError.from(cause,
|
|
135
|
+
return Result.fail(AndcoError.from(cause, ANDCO_ERROR_CODES.REFRESH_FAILED));
|
|
132
136
|
}
|
|
133
137
|
}
|
|
134
138
|
/** Reads the authenticated subject for one access token. */
|
|
@@ -137,25 +141,48 @@ export class AndcoOAuth {
|
|
|
137
141
|
// The subject check compares the UserInfo response against the token's own `sub`, which
|
|
138
142
|
// exists only when the token is a JWT. An opaque token offers nothing to compare, so the
|
|
139
143
|
// check is skipped rather than failing a request that is otherwise valid.
|
|
140
|
-
const subject =
|
|
141
|
-
const info = await oidc.fetchUserInfo(this.#oidc(), accessToken, subject ? subject
|
|
144
|
+
const subject = decodeJwtPayload(accessToken)?.["sub"];
|
|
145
|
+
const info = await oidc.fetchUserInfo(this.#oidc(), accessToken, typeof subject === "string" ? subject : oidc.skipSubjectCheck);
|
|
142
146
|
return Result.ok({
|
|
143
147
|
id: String(info.sub),
|
|
144
|
-
name: info.name,
|
|
145
|
-
email: info.email,
|
|
146
|
-
avatarUrl: info.picture,
|
|
148
|
+
name: typeof info.name === "string" ? info.name : null,
|
|
149
|
+
email: typeof info.email === "string" ? info.email : null,
|
|
150
|
+
avatarUrl: typeof info.picture === "string" ? info.picture : null,
|
|
151
|
+
emailVerified: typeof info.email_verified === "boolean" ? info.email_verified : null,
|
|
152
|
+
phone: typeof info["phone"] === "string" ? info["phone"] : null,
|
|
153
|
+
phoneVerified: typeof info["phone_verified"] === "boolean" ? info["phone_verified"] : null,
|
|
147
154
|
});
|
|
148
155
|
}
|
|
149
156
|
catch (cause) {
|
|
150
|
-
return Result.fail(AndcoError.from(cause,
|
|
157
|
+
return Result.fail(AndcoError.from(cause, ANDCO_ERROR_CODES.USERINFO_FAILED));
|
|
151
158
|
}
|
|
152
159
|
}
|
|
153
|
-
/**
|
|
160
|
+
/**
|
|
161
|
+
* Starts RFC 8628 Device Authorization for a runtime that cannot present a browser.
|
|
162
|
+
*
|
|
163
|
+
* Takes the same permissions as {@link createAuthorizationRequest}: scopes, Resource Server
|
|
164
|
+
* contributions (bound to their server through the same plan, with one `resource` parameter
|
|
165
|
+
* each) and loose `authorizationDetails`. They travel in the device request's form body, which
|
|
166
|
+
* the Authorization Server records exactly as it records a pushed request.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```ts
|
|
170
|
+
* const { data: device } = await andco.oauth.createDeviceAuthorizationRequest({
|
|
171
|
+
* scopes: ["openid", "email"],
|
|
172
|
+
* authorizations: [bank.accounts.authorization({ actions: ["read"], subject: { type: "profile" } })],
|
|
173
|
+
* });
|
|
174
|
+
* console.log(device.verificationUriComplete, device.userCode);
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
154
177
|
async createDeviceAuthorizationRequest(options = {}) {
|
|
155
178
|
try {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
179
|
+
const parameters = new URLSearchParams();
|
|
180
|
+
const scopes = this.#scopes(options);
|
|
181
|
+
if (scopes.length)
|
|
182
|
+
parameters.set("scope", scopes.join(" "));
|
|
183
|
+
this.#appendPermissions(parameters, options);
|
|
184
|
+
console.debug("[AndcoOAuth.createDeviceAuthorizationRequest] scopes %s resources %o details %s", scopes.join(" "), parameters.getAll("resource"), parameters.has("authorization_details"));
|
|
185
|
+
const response = await oidc.initiateDeviceAuthorization(this.#oidc(), parameters);
|
|
159
186
|
return Result.ok({
|
|
160
187
|
deviceCode: response.device_code,
|
|
161
188
|
userCode: response.user_code,
|
|
@@ -167,7 +194,7 @@ export class AndcoOAuth {
|
|
|
167
194
|
});
|
|
168
195
|
}
|
|
169
196
|
catch (cause) {
|
|
170
|
-
return Result.fail(AndcoError.from(cause,
|
|
197
|
+
return Result.fail(AndcoError.from(cause, ANDCO_ERROR_CODES.DEVICE_AUTHORIZATION_FAILED));
|
|
171
198
|
}
|
|
172
199
|
}
|
|
173
200
|
/**
|
|
@@ -185,7 +212,7 @@ export class AndcoOAuth {
|
|
|
185
212
|
return await this.#sessionFrom(tokens);
|
|
186
213
|
}
|
|
187
214
|
catch (cause) {
|
|
188
|
-
return Result.fail(AndcoError.from(cause,
|
|
215
|
+
return Result.fail(AndcoError.from(cause, ANDCO_ERROR_CODES.DEVICE_EXCHANGE_FAILED));
|
|
189
216
|
}
|
|
190
217
|
}
|
|
191
218
|
/**
|
|
@@ -226,7 +253,7 @@ export class AndcoOAuth {
|
|
|
226
253
|
const configuration = new oidc.Configuration(andcoServerMetadata(issuer), config.clientId, secret ?? undefined, secret ? oidc.ClientSecretBasic(secret) : oidc.None());
|
|
227
254
|
// `CustomFetch` widens the body to include `Uint8Array`, which every runtime's `fetch` accepts
|
|
228
255
|
// 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));
|
|
256
|
+
configuration[oidc.customFetch] = ((input, init) => this.#globals.fetch(input, init));
|
|
230
257
|
// The Andco Authorization Server is reachable over loopback HTTP in local development.
|
|
231
258
|
oidc.allowInsecureRequests(configuration);
|
|
232
259
|
return configuration;
|
|
@@ -245,6 +272,30 @@ export class AndcoOAuth {
|
|
|
245
272
|
const requested = options.scopes ?? this.#config.initialScopes;
|
|
246
273
|
return [...new Set([...requested, ...contributed])];
|
|
247
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Appends the `resource` parameters and `authorization_details` both the browser and the device
|
|
277
|
+
* request carry. Each Resource Server contribution goes through the protocol's plan, which binds
|
|
278
|
+
* its details to that server (`locations: [resource]`) and refuses details naming different
|
|
279
|
+
* subjects, loose `authorizationDetails` included. A detail sent without `locations` does not match the server's registered schema and
|
|
280
|
+
* Andco cannot record it.
|
|
281
|
+
*/
|
|
282
|
+
#appendPermissions(parameters, options) {
|
|
283
|
+
const resources = new Set();
|
|
284
|
+
for (const value of toArray(options.resource))
|
|
285
|
+
resources.add(value);
|
|
286
|
+
for (const contribution of options.authorizations ?? [])
|
|
287
|
+
resources.add(contribution.resource);
|
|
288
|
+
for (const resource of resources)
|
|
289
|
+
parameters.append("resource", resource);
|
|
290
|
+
// Loose details join the plan as a contribution without a resource: they keep their own
|
|
291
|
+
// `locations`, and their subjects are compared with every contribution's.
|
|
292
|
+
const plan = createAndCoAuthorizationPlan([
|
|
293
|
+
{ authorizationDetails: options.authorizationDetails ?? [] },
|
|
294
|
+
...(options.authorizations ?? []),
|
|
295
|
+
]);
|
|
296
|
+
if (plan.authorizationDetails.length)
|
|
297
|
+
parameters.set("authorization_details", JSON.stringify(plan.authorizationDetails));
|
|
298
|
+
}
|
|
248
299
|
#authorizationParameters(options, redirectTo, state, codeChallenge) {
|
|
249
300
|
const parameters = new URLSearchParams({
|
|
250
301
|
client_id: this.#config.clientId,
|
|
@@ -257,22 +308,7 @@ export class AndcoOAuth {
|
|
|
257
308
|
const scopes = this.#scopes(options);
|
|
258
309
|
if (scopes.length)
|
|
259
310
|
parameters.set("scope", scopes.join(" "));
|
|
260
|
-
|
|
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);
|
|
311
|
+
this.#appendPermissions(parameters, options);
|
|
276
312
|
if (options.externalId)
|
|
277
313
|
parameters.set("external_id", options.externalId);
|
|
278
314
|
return parameters;
|
|
@@ -299,21 +335,30 @@ export class AndcoOAuth {
|
|
|
299
335
|
}
|
|
300
336
|
async #sessionFrom(tokens) {
|
|
301
337
|
if (tokens.token_type && tokens.token_type.toLowerCase() !== "bearer") {
|
|
302
|
-
return Result.fail(ANDCO_ERROR_CODES.
|
|
338
|
+
return Result.fail(ANDCO_ERROR_CODES.INVALID_TOKEN_RESPONSE, { message: "unsupported token type" });
|
|
303
339
|
}
|
|
304
340
|
// The ID Token is where OpenID Connect puts the subject. Reading it out of the access token
|
|
305
341
|
// works only while that token happens to be a JWT, which no specification promises and an
|
|
306
342
|
// Authorization Server may stop doing without warning.
|
|
307
|
-
const
|
|
343
|
+
const accessSubject = decodeJwtPayload(tokens.access_token)?.["sub"];
|
|
344
|
+
const subject = tokens.claims?.()?.sub ?? (typeof accessSubject === "string" ? accessSubject : undefined);
|
|
308
345
|
if (!subject)
|
|
309
|
-
return Result.fail(ANDCO_ERROR_CODES.
|
|
346
|
+
return Result.fail(ANDCO_ERROR_CODES.INVALID_TOKEN_RESPONSE, { message: "the response carries no subject" });
|
|
310
347
|
const session = {
|
|
311
348
|
accessToken: tokens.access_token,
|
|
312
349
|
refreshToken: tokens.refresh_token ?? null,
|
|
313
350
|
tokenType: tokens.token_type ?? "bearer",
|
|
314
351
|
expiresAt: Math.floor(Date.now() / 1000) + (tokens.expires_in ?? 0),
|
|
315
352
|
scopes: (tokens.scope ?? "").split(/\s+/).filter(Boolean),
|
|
316
|
-
user: {
|
|
353
|
+
user: {
|
|
354
|
+
id: subject,
|
|
355
|
+
name: null,
|
|
356
|
+
email: null,
|
|
357
|
+
avatarUrl: null,
|
|
358
|
+
emailVerified: null,
|
|
359
|
+
phone: null,
|
|
360
|
+
phoneVerified: null,
|
|
361
|
+
},
|
|
317
362
|
};
|
|
318
363
|
const user = await this.userInfo(session.accessToken);
|
|
319
364
|
return Result.ok(user.data ? { ...session, user: user.data } : session);
|
|
@@ -330,19 +375,3 @@ function callbackMatchesRedirect(actual, expected) {
|
|
|
330
375
|
return false;
|
|
331
376
|
return [...expected.searchParams].every(([key, value]) => actual.searchParams.getAll(key).includes(value));
|
|
332
377
|
}
|
|
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
|
-
}
|
package/dist/presenter.d.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Result } from "./errors.js";
|
|
2
2
|
/** How an authorization is put in front of the user. */
|
|
3
3
|
export type AndcoPresentation = "popup" | "redirect";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
/**
|
|
5
|
+
* How an Intent is put in front of the user. `"newtab"` is a popup without window features: same
|
|
6
|
+
* relay back to the opener, for where popups are blocked.
|
|
7
|
+
*/
|
|
8
|
+
export type AndcoIntentPresentation = "popup" | "newtab" | "redirect";
|
|
9
|
+
/**
|
|
10
|
+
* The destination, or a thunk that resolves it.
|
|
11
|
+
*
|
|
12
|
+
* A thunk is what lets a presenter open its window *during* the user activation and navigate it
|
|
13
|
+
* afterwards. Both flows need it: an authorization request may be pushed (PAR) before its URL
|
|
14
|
+
* exists, and an Intent is created only once someone asked for it. Awaiting either before opening
|
|
15
|
+
* is what gets the window blocked. A thunk that cannot resolve throws an {@link AndcoError} rather
|
|
16
|
+
* than returning one; {@link AndcoPresenter.present} is what turns that back into a `Result` for a presenter.
|
|
17
|
+
*/
|
|
18
|
+
export type AndcoPresentTarget = URL | (() => Promise<URL>);
|
|
19
|
+
type AndcoPresentBase = {
|
|
20
|
+
url: AndcoPresentTarget;
|
|
7
21
|
/** Exact callback the Authorization Server will reach on success. */
|
|
8
22
|
returnTo: URL;
|
|
9
23
|
/** Exact callback reached on failure. Defaults to `returnTo`. */
|
|
@@ -12,11 +26,34 @@ export type AndcoPresentOptions = {
|
|
|
12
26
|
presentationId: string;
|
|
13
27
|
signal?: AbortSignal;
|
|
14
28
|
};
|
|
29
|
+
/** An authorization. Its callback carries a single-use code the opener must exchange. */
|
|
30
|
+
export type AndcoPresentOAuth = AndcoPresentBase & {
|
|
31
|
+
kind: "oauth";
|
|
32
|
+
presentation: AndcoPresentation;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* An Intent. Its callback carries a lifecycle outcome only — the authenticated read decides the
|
|
36
|
+
* financial fact — and `intentId` is what lets a presenter with no window, such as a native Host,
|
|
37
|
+
* resolve current state from the Resource Server instead.
|
|
38
|
+
*/
|
|
39
|
+
export type AndcoPresentIntent = AndcoPresentBase & {
|
|
40
|
+
kind: "intent";
|
|
41
|
+
/** A `"redirect"` that navigates answers `null`; its result is read by `andco.intents.fromCallback`. */
|
|
42
|
+
presentation: AndcoIntentPresentation;
|
|
43
|
+
/** Empty until the thunk has resolved; a presenter reads it after resolution, never before. */
|
|
44
|
+
readonly intentId: string;
|
|
45
|
+
};
|
|
46
|
+
export type AndcoPresentOptions = AndcoPresentOAuth | AndcoPresentIntent;
|
|
15
47
|
/**
|
|
16
48
|
* The boundary between the protocol and whatever shows it to a person.
|
|
17
49
|
*
|
|
18
|
-
* A presenter receives
|
|
19
|
-
*
|
|
50
|
+
* A presenter receives a destination and answers with the callback URL that came back, or `null`
|
|
51
|
+
* when the user dismissed. It owns no protocol: no PKCE, no state, no token exchange, and no
|
|
52
|
+
* opinion about what a completed Intent means.
|
|
53
|
+
*
|
|
54
|
+
* `kind` is the seam's only domain knowledge, and it exists because a presenter without a window
|
|
55
|
+
* cannot treat the two alike: a native Host runs its own consent screen for an authorization and
|
|
56
|
+
* its own review screen for an Intent. A browser presents both the same way and ignores it.
|
|
20
57
|
*
|
|
21
58
|
* Keeping it this narrow is what makes sign-in testable without a browser — a test supplies a
|
|
22
59
|
* presenter that returns a prepared callback URL and the whole flow runs in plain Node. It is also
|
|
@@ -25,12 +62,23 @@ export type AndcoPresentOptions = {
|
|
|
25
62
|
*
|
|
26
63
|
* @example
|
|
27
64
|
* ```ts
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* };
|
|
65
|
+
* class Presenter extends AndcoPresenter {
|
|
66
|
+
* async present({ returnTo }: AndcoPresentOptions) {
|
|
67
|
+
* return Result.ok(new URL(`${returnTo}?code=test&state=${state}`));
|
|
68
|
+
* }
|
|
69
|
+
* }
|
|
31
70
|
* ```
|
|
32
71
|
*/
|
|
33
|
-
export
|
|
34
|
-
|
|
72
|
+
export declare abstract class AndcoPresenter {
|
|
73
|
+
/** Resolves a presentation target, whether it was already a URL or still a thunk. */
|
|
74
|
+
static present(target: AndcoPresentTarget): Promise<Result<URL>>;
|
|
75
|
+
abstract present(options: AndcoPresentOptions): Promise<Result<URL | null>>;
|
|
76
|
+
/**
|
|
77
|
+
* Brings the window of a presentation that is still open to the front, so presenting the same
|
|
78
|
+
* Intent twice shows the one window instead of opening a second. A presenter with no window of
|
|
79
|
+
* its own, such as a native Host that owns its screen, simply omits it.
|
|
80
|
+
*/
|
|
81
|
+
focus?(presentationId: string): void;
|
|
35
82
|
}
|
|
83
|
+
export {};
|
|
36
84
|
//# sourceMappingURL=presenter.d.ts.map
|
package/dist/presenter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"presenter.d.ts","sourceRoot":"","sources":["../src/presenter.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"presenter.d.ts","sourceRoot":"","sources":["../src/presenter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiC,MAAM,EAAE,MAAM,aAAa,CAAC;AAEpE,wDAAwD;AACxD,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,UAAU,CAAC;AAErD;;;GAGG;AACH,MAAM,MAAM,uBAAuB,GAAG,OAAO,GAAG,QAAQ,GAAG,UAAU,CAAC;AAEtE;;;;;;;;GAQG;AACH,MAAM,MAAM,kBAAkB,GAAG,GAAG,GAAG,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;AAE5D,KAAK,gBAAgB,GAAG;IACtB,GAAG,EAAE,kBAAkB,CAAC;IACxB,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,yFAAyF;AACzF,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG;IACjD,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,iBAAiB,CAAC;CACjC,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,gBAAgB,GAAG;IAClD,IAAI,EAAE,QAAQ,CAAC;IACf,wGAAwG;IACxG,YAAY,EAAE,uBAAuB,CAAC;IACtC,+FAA+F;IAC/F,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,iBAAiB,GAAG,kBAAkB,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,8BAAsB,cAAc;IAClC,qFAAqF;WACxE,OAAO,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAWtE,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;IAE3E;;;;OAIG;IACH,KAAK,CAAC,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI;CACrC"}
|
package/dist/presenter.js
CHANGED
|
@@ -1 +1,40 @@
|
|
|
1
|
-
|
|
1
|
+
import { ANDCO_ERROR_CODES, AndcoError, Result } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* The boundary between the protocol and whatever shows it to a person.
|
|
4
|
+
*
|
|
5
|
+
* A presenter receives a destination and answers with the callback URL that came back, or `null`
|
|
6
|
+
* when the user dismissed. It owns no protocol: no PKCE, no state, no token exchange, and no
|
|
7
|
+
* opinion about what a completed Intent means.
|
|
8
|
+
*
|
|
9
|
+
* `kind` is the seam's only domain knowledge, and it exists because a presenter without a window
|
|
10
|
+
* cannot treat the two alike: a native Host runs its own consent screen for an authorization and
|
|
11
|
+
* its own review screen for an Intent. A browser presents both the same way and ignores it.
|
|
12
|
+
*
|
|
13
|
+
* Keeping it this narrow is what makes sign-in testable without a browser — a test supplies a
|
|
14
|
+
* presenter that returns a prepared callback URL and the whole flow runs in plain Node. It is also
|
|
15
|
+
* why a redirect presentation can answer with `null` and never resolve: the document is replaced,
|
|
16
|
+
* and the result arrives on the next page load instead.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* class Presenter extends AndcoPresenter {
|
|
21
|
+
* async present({ returnTo }: AndcoPresentOptions) {
|
|
22
|
+
* return Result.ok(new URL(`${returnTo}?code=test&state=${state}`));
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export class AndcoPresenter {
|
|
28
|
+
/** Resolves a presentation target, whether it was already a URL or still a thunk. */
|
|
29
|
+
static async present(target) {
|
|
30
|
+
if (target instanceof URL) {
|
|
31
|
+
return Result.ok(target);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return Result.ok(await target());
|
|
35
|
+
}
|
|
36
|
+
catch (cause) {
|
|
37
|
+
return Result.fail(AndcoError.from(cause, ANDCO_ERROR_CODES.PRESENTATION_TARGET_FAILED));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Result } from "./errors.js";
|
|
2
|
+
import type { AndcoGrantAuthorization, AndcoGrants } from "./grants-api.js";
|
|
3
|
+
import type { AndcoTokens } from "./tokens.js";
|
|
4
|
+
/** One RAR detail a route needs the Grant to carry: the type, and every action it uses. */
|
|
5
|
+
export type AndcoDetailRequirement = {
|
|
6
|
+
readonly type: string;
|
|
7
|
+
readonly actions?: readonly string[];
|
|
8
|
+
};
|
|
9
|
+
/** What a protected route requires of the caller's Grant. Everything omitted is not checked. */
|
|
10
|
+
export type AndcoProtectOptions = {
|
|
11
|
+
/** Scopes the Grant must carry, all of them. */
|
|
12
|
+
scopes?: readonly string[];
|
|
13
|
+
/** RAR details the Grant must carry; each needs a detail of that type including its actions. */
|
|
14
|
+
authorizationDetails?: readonly AndcoDetailRequirement[];
|
|
15
|
+
/**
|
|
16
|
+
* Verify the token offline first (signature, `exp`, `iss`, and `aud` equal to the resource), so a
|
|
17
|
+
* forged token never costs a request to Andco. Off by default: it assumes the Authorization Server
|
|
18
|
+
* addresses tokens to this resource.
|
|
19
|
+
*/
|
|
20
|
+
verify?: boolean;
|
|
21
|
+
};
|
|
22
|
+
/** A guard answered `Response` to reject the request, or the live Grant to let it through. */
|
|
23
|
+
export type AndcoProtectOutcome = Result<AndcoGrantAuthorization> | Response;
|
|
24
|
+
/** Hono's `Context`, as far as the adapter needs it. */
|
|
25
|
+
type HonoLikeContext = {
|
|
26
|
+
req: {
|
|
27
|
+
raw: Request;
|
|
28
|
+
};
|
|
29
|
+
set(key: "andcoGrant", value: AndcoGrantAuthorization): void;
|
|
30
|
+
};
|
|
31
|
+
/** Express's request, as far as the adapter needs it. */
|
|
32
|
+
type ExpressLikeRequest = {
|
|
33
|
+
headers: Record<string, string | string[] | undefined>;
|
|
34
|
+
};
|
|
35
|
+
/** Express's response, as far as the adapter needs it. */
|
|
36
|
+
type ExpressLikeResponse = {
|
|
37
|
+
locals: Record<string, unknown>;
|
|
38
|
+
status(code: number): unknown;
|
|
39
|
+
setHeader(name: string, value: string): unknown;
|
|
40
|
+
send(body: string): unknown;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* A route guard built by {@link AndcoResource.protect}. Call it with a Fetch `Request`, or plug one
|
|
44
|
+
* of its adapters into a framework.
|
|
45
|
+
*/
|
|
46
|
+
export type AndcoProtection = {
|
|
47
|
+
(request: Request): Promise<AndcoProtectOutcome>;
|
|
48
|
+
/** Hono middleware. The Grant is available as `c.get("andcoGrant")`. */
|
|
49
|
+
hono(): (context: HonoLikeContext, next: () => Promise<void>) => Promise<Response | undefined>;
|
|
50
|
+
/** Express middleware. The Grant is available as `res.locals.andcoGrant`. */
|
|
51
|
+
express(): (request: ExpressLikeRequest, response: ExpressLikeResponse, next: () => void) => Promise<void>;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* One Resource Server's view of Andco: verify the bearer token, fetch the live Grant, and answer
|
|
55
|
+
* 401 or 403 the way RFC 6750 and RFC 9728 describe.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* const orders = andco.resource({ resource: "https://casa-norte.example/api" });
|
|
60
|
+
*
|
|
61
|
+
* // Fetch-style handler (Next route handlers, Workers, Bun, Deno)
|
|
62
|
+
* export async function GET(request: Request) {
|
|
63
|
+
* const outcome = await orders.protect({ scopes: ["orders:read"] })(request);
|
|
64
|
+
* if (outcome instanceof Response) return outcome; // 401 or 403, already built
|
|
65
|
+
* return Response.json(await listOrders(outcome.data.subject));
|
|
66
|
+
* }
|
|
67
|
+
*
|
|
68
|
+
* // Hono
|
|
69
|
+
* app.use("/api/orders/*", orders.protect({ authorizationDetails: [{ type: "orders", actions: ["read"] }] }).hono());
|
|
70
|
+
*
|
|
71
|
+
* // Express
|
|
72
|
+
* router.get("/api/orders", orders.protect({ scopes: ["orders:read"] }).express(), handler);
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export declare class AndcoResource {
|
|
76
|
+
#private;
|
|
77
|
+
readonly resource: string;
|
|
78
|
+
/** Where `WWW-Authenticate` points clients for this resource's metadata (RFC 9728). */
|
|
79
|
+
readonly resourceMetadata: string;
|
|
80
|
+
constructor(options: {
|
|
81
|
+
resource: string;
|
|
82
|
+
/** Overrides the RFC 9728 well-known URL derived from `resource`. */
|
|
83
|
+
resourceMetadata?: string;
|
|
84
|
+
tokens: AndcoTokens;
|
|
85
|
+
grants: AndcoGrants;
|
|
86
|
+
});
|
|
87
|
+
/**
|
|
88
|
+
* The document to publish at {@link resourceMetadata}, so clients can discover the Authorization Server.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* app.get("/.well-known/oauth-protected-resource/api", (_req, res) =>
|
|
93
|
+
* res.json(orders.metadata({ scopesSupported: ["orders:read"] })),
|
|
94
|
+
* );
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
metadata(options?: {
|
|
98
|
+
scopesSupported?: readonly string[];
|
|
99
|
+
}): {
|
|
100
|
+
resource: string;
|
|
101
|
+
authorization_servers: string[];
|
|
102
|
+
bearer_methods_supported: string[];
|
|
103
|
+
scopes_supported: readonly string[] | undefined;
|
|
104
|
+
};
|
|
105
|
+
/** Builds a guard requiring a live Grant for this resource that satisfies `options`. */
|
|
106
|
+
protect(options?: AndcoProtectOptions): AndcoProtection;
|
|
107
|
+
}
|
|
108
|
+
export {};
|
|
109
|
+
//# sourceMappingURL=resource.d.ts.map
|