@soulcode/bexio-oauth 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +66 -2
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +4 -0
- package/dist/bin.js.map +1 -0
- package/dist/claims.d.ts +31 -0
- package/dist/claims.d.ts.map +1 -0
- package/dist/claims.js +58 -0
- package/dist/claims.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +44 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +24 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +57 -0
- package/dist/config.js.map +1 -0
- package/dist/connect-flow.d.ts +15 -0
- package/dist/connect-flow.d.ts.map +1 -0
- package/dist/connect-flow.js +123 -0
- package/dist/connect-flow.js.map +1 -0
- package/dist/crypto.d.ts +27 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +106 -0
- package/dist/crypto.js.map +1 -0
- package/dist/duration.d.ts +4 -0
- package/dist/duration.d.ts.map +1 -0
- package/dist/duration.js +15 -0
- package/dist/duration.js.map +1 -0
- package/dist/errors.d.ts +38 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -0
- package/dist/external-tokens.d.ts +8 -0
- package/dist/external-tokens.d.ts.map +1 -0
- package/dist/external-tokens.js +73 -0
- package/dist/external-tokens.js.map +1 -0
- package/dist/flow-cookie.d.ts +23 -0
- package/dist/flow-cookie.d.ts.map +1 -0
- package/dist/flow-cookie.js +44 -0
- package/dist/flow-cookie.js.map +1 -0
- package/dist/grants.d.ts +36 -0
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +115 -0
- package/dist/grants.js.map +1 -0
- package/dist/idle-sweep.d.ts +11 -0
- package/dist/idle-sweep.d.ts.map +1 -0
- package/dist/idle-sweep.js +70 -0
- package/dist/idle-sweep.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations.generated.d.ts +3 -0
- package/dist/migrations.generated.d.ts.map +1 -0
- package/dist/migrations.generated.js +40 -0
- package/dist/migrations.generated.js.map +1 -0
- package/dist/oidc.d.ts +30 -0
- package/dist/oidc.d.ts.map +1 -0
- package/dist/oidc.js +67 -0
- package/dist/oidc.js.map +1 -0
- package/dist/permissions.d.ts +9 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +29 -0
- package/dist/permissions.js.map +1 -0
- package/dist/refresh.d.ts +30 -0
- package/dist/refresh.d.ts.map +1 -0
- package/dist/refresh.js +139 -0
- package/dist/refresh.js.map +1 -0
- package/dist/return-to.d.ts +8 -0
- package/dist/return-to.d.ts.map +1 -0
- package/dist/return-to.js +31 -0
- package/dist/return-to.js.map +1 -0
- package/dist/sql.d.ts +14 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +11 -0
- package/dist/sql.js.map +1 -0
- package/dist/store/memory.d.ts +7 -0
- package/dist/store/memory.d.ts.map +1 -0
- package/dist/store/memory.js +104 -0
- package/dist/store/memory.js.map +1 -0
- package/dist/store/postgres.d.ts +17 -0
- package/dist/store/postgres.d.ts.map +1 -0
- package/dist/store/postgres.js +177 -0
- package/dist/store/postgres.js.map +1 -0
- package/dist/store/types.d.ts +105 -0
- package/dist/store/types.d.ts.map +1 -0
- package/dist/store/types.js +2 -0
- package/dist/store/types.js.map +1 -0
- package/dist/types.d.ts +150 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +60 -4
- package/sql/0001_bexio_grants.sql +32 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Soulcode
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,67 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @soulcode/bexio-oauth
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Bexio OAuth token custody for Node apps. Connect Bexio accounts, store their tokens encrypted in Postgres, and get a valid access token anywhere, including background jobs, without two server instances ever refreshing the same token at once.
|
|
4
|
+
|
|
5
|
+
It is **not** an auth framework: your app keeps its own login. For "Sign in with Bexio", let your auth framework run the login and hand the tokens over with `saveGrant()`.
|
|
6
|
+
|
|
7
|
+
- Full guide: [docs/integration.md](https://github.com/SoulcodeAgency/bexio-oauth/blob/main/docs/integration.md)
|
|
8
|
+
- Working examples (Better Auth, Supabase Auth): [examples/](https://github.com/SoulcodeAgency/bexio-oauth/tree/main/examples)
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @soulcode/bexio-oauth pg
|
|
14
|
+
npx @soulcode/bexio-oauth sql > migrations/0001_bexio_grants.sql # then apply it
|
|
15
|
+
npx @soulcode/bexio-oauth keygen # encryption key
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Requires Node ≥ 24 and PostgreSQL.
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// lib/bexio.ts (server-only)
|
|
24
|
+
import { Pool } from "pg";
|
|
25
|
+
import { createBexioOAuth, postgresStore } from "@soulcode/bexio-oauth";
|
|
26
|
+
|
|
27
|
+
export const bexio = createBexioOAuth({
|
|
28
|
+
clientId: process.env.BEXIO_CLIENT_ID!,
|
|
29
|
+
clientSecret: process.env.BEXIO_CLIENT_SECRET!,
|
|
30
|
+
redirectUri: process.env.BEXIO_REDIRECT_URI!, // e.g. https://app.example.com/api/bexio/callback
|
|
31
|
+
scopes: ["contact_show"],
|
|
32
|
+
keys: JSON.parse(process.env.BEXIO_TOKEN_KEYS!), // {"current":"k1","keys":{"k1":"<keygen>"}}
|
|
33
|
+
store: postgresStore(
|
|
34
|
+
new Pool({ connectionString: process.env.DATABASE_URL }),
|
|
35
|
+
),
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// GET /api/bexio/connect
|
|
41
|
+
export const GET = async (req: Request) =>
|
|
42
|
+
bexio.start({
|
|
43
|
+
ownerId: await currentWorkspaceId(req),
|
|
44
|
+
returnTo: "/settings",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// GET /api/bexio/callback (your redirectUri)
|
|
48
|
+
export const GET = async (req: Request) =>
|
|
49
|
+
bexio.callback(req, { ownerId: await currentWorkspaceId(req) });
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// anywhere on the server, including cron jobs
|
|
54
|
+
const { accessToken } = await bexio.getAccessToken({ ownerId: workspaceId });
|
|
55
|
+
await fetch("https://api.bexio.com/2.0/contact", {
|
|
56
|
+
headers: {
|
|
57
|
+
Authorization: `Bearer ${accessToken}`,
|
|
58
|
+
Accept: "application/json",
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Also: `listGrants`, `disconnect`, `refreshIdle` (keep idle grants alive), `saveGrant` (tokens from your auth framework). Every error is a `BexioOAuthError` with a stable `code`.
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
MIT
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
|
package/dist/bin.js
ADDED
package/dist/bin.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,OAAO,CAAC,QAAQ,GAAG,MAAM,GAAG,CAC1B,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EACrB,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAC9B,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAC/B,CAAC"}
|
package/dist/claims.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Bexio's production OIDC issuer (Keycloak realm). */
|
|
2
|
+
export declare const BEXIO_ISSUER = "https://auth.bexio.com/realms/bexio";
|
|
3
|
+
/** Bexio's production API base URL. */
|
|
4
|
+
export declare const BEXIO_API_BASE_URL = "https://api.bexio.com";
|
|
5
|
+
/**
|
|
6
|
+
* Always requested. `company_profile` adds the company claims; `offline_access`
|
|
7
|
+
* issues a refresh token that keeps working without the user present.
|
|
8
|
+
*/
|
|
9
|
+
export declare const BASE_SCOPES: readonly string[];
|
|
10
|
+
export type ClaimBag = Record<string, unknown>;
|
|
11
|
+
/** Who granted access, as read from Bexio's ID token, access token and userinfo. */
|
|
12
|
+
export interface BexioIdentity {
|
|
13
|
+
/** OIDC `sub`. */
|
|
14
|
+
userId: string;
|
|
15
|
+
/** `company_id` (company_profile scope). */
|
|
16
|
+
companyId: string;
|
|
17
|
+
companyName: string | null;
|
|
18
|
+
/** `company_user_id` (access token): the user's id inside the company. */
|
|
19
|
+
companyUserId: number | null;
|
|
20
|
+
userName: string | null;
|
|
21
|
+
userEmail: string | null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Maps a merged claim bag (ID token + access token + userinfo) to a
|
|
25
|
+
* `BexioIdentity`. Throws `invalid_callback` when `sub` or `company_id` is
|
|
26
|
+
* missing.
|
|
27
|
+
*/
|
|
28
|
+
export declare function mapClaims(claims: ClaimBag): BexioIdentity;
|
|
29
|
+
/** The value a grant's ciphertext is bound to (see `TokenCipher`). */
|
|
30
|
+
export declare function grantBinding(ownerId: string, companyId: string, userId: string): string;
|
|
31
|
+
//# sourceMappingURL=claims.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claims.d.ts","sourceRoot":"","sources":["../src/claims.ts"],"names":[],"mappings":"AAEA,uDAAuD;AACvD,eAAO,MAAM,YAAY,wCAAwC,CAAC;AAClE,uCAAuC;AACvC,eAAO,MAAM,kBAAkB,0BAA0B,CAAC;AAC1D;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,SAAS,MAAM,EAMvC,CAAC;AAEH,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE/C,oFAAoF;AACpF,MAAM,WAAW,aAAa;IAC5B,kBAAkB;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,0EAA0E;IAC1E,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAaD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,QAAQ,GAAG,aAAa,CAwBzD;AAED,sEAAsE;AACtE,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,MAAM,CAER"}
|
package/dist/claims.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { BexioOAuthError } from "./errors.js";
|
|
2
|
+
/** Bexio's production OIDC issuer (Keycloak realm). */
|
|
3
|
+
export const BEXIO_ISSUER = "https://auth.bexio.com/realms/bexio";
|
|
4
|
+
/** Bexio's production API base URL. */
|
|
5
|
+
export const BEXIO_API_BASE_URL = "https://api.bexio.com";
|
|
6
|
+
/**
|
|
7
|
+
* Always requested. `company_profile` adds the company claims; `offline_access`
|
|
8
|
+
* issues a refresh token that keeps working without the user present.
|
|
9
|
+
*/
|
|
10
|
+
export const BASE_SCOPES = Object.freeze([
|
|
11
|
+
"openid",
|
|
12
|
+
"profile",
|
|
13
|
+
"email",
|
|
14
|
+
"company_profile",
|
|
15
|
+
"offline_access",
|
|
16
|
+
]);
|
|
17
|
+
function text(value) {
|
|
18
|
+
if (typeof value === "string" && value.length > 0)
|
|
19
|
+
return value;
|
|
20
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
21
|
+
return String(value);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function integer(value) {
|
|
25
|
+
const parsed = typeof value === "string" ? Number.parseInt(value, 10) : value;
|
|
26
|
+
return typeof parsed === "number" && Number.isInteger(parsed) ? parsed : null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Maps a merged claim bag (ID token + access token + userinfo) to a
|
|
30
|
+
* `BexioIdentity`. Throws `invalid_callback` when `sub` or `company_id` is
|
|
31
|
+
* missing.
|
|
32
|
+
*/
|
|
33
|
+
export function mapClaims(claims) {
|
|
34
|
+
const userId = text(claims.sub);
|
|
35
|
+
if (!userId)
|
|
36
|
+
throw new BexioOAuthError("invalid_callback", "missing sub claim");
|
|
37
|
+
const companyId = text(claims.company_id);
|
|
38
|
+
if (!companyId) {
|
|
39
|
+
throw new BexioOAuthError("invalid_callback", "missing company_id claim (is the company_profile scope requested?)");
|
|
40
|
+
}
|
|
41
|
+
const given = text(claims.given_name);
|
|
42
|
+
const family = text(claims.family_name);
|
|
43
|
+
return {
|
|
44
|
+
userId,
|
|
45
|
+
companyId,
|
|
46
|
+
companyName: text(claims.company_name),
|
|
47
|
+
companyUserId: integer(claims.company_user_id),
|
|
48
|
+
userName: given && family
|
|
49
|
+
? `${given} ${family}`
|
|
50
|
+
: (text(claims.name) ?? given ?? family),
|
|
51
|
+
userEmail: text(claims.email),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** The value a grant's ciphertext is bound to (see `TokenCipher`). */
|
|
55
|
+
export function grantBinding(ownerId, companyId, userId) {
|
|
56
|
+
return `${ownerId}\u0000${companyId}\u0000${userId}`;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=claims.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claims.js","sourceRoot":"","sources":["../src/claims.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,uDAAuD;AACvD,MAAM,CAAC,MAAM,YAAY,GAAG,qCAAqC,CAAC;AAClE,uCAAuC;AACvC,MAAM,CAAC,MAAM,kBAAkB,GAAG,uBAAuB,CAAC;AAC1D;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAsB,MAAM,CAAC,MAAM,CAAC;IAC1D,QAAQ;IACR,SAAS;IACT,OAAO;IACP,iBAAiB;IACjB,gBAAgB;CACjB,CAAC,CAAC;AAiBH,SAAS,IAAI,CAAC,KAAc;IAC1B,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAChE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IAC9E,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,OAAO,CAAC,KAAc;IAC7B,MAAM,MAAM,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IAC9E,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;AAChF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,MAAgB;IACxC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAChC,IAAI,CAAC,MAAM;QACT,MAAM,IAAI,eAAe,CAAC,kBAAkB,EAAE,mBAAmB,CAAC,CAAC;IACrE,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAC1C,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,IAAI,eAAe,CACvB,kBAAkB,EAClB,oEAAoE,CACrE,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IACtC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IACxC,OAAO;QACL,MAAM;QACN,SAAS;QACT,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC;QACtC,aAAa,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe,CAAC;QAC9C,QAAQ,EACN,KAAK,IAAI,MAAM;YACb,CAAC,CAAC,GAAG,KAAK,IAAI,MAAM,EAAE;YACtB,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,IAAI,MAAM,CAAC;QAC5C,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;KAC9B,CAAC;AACJ,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,YAAY,CAC1B,OAAe,EACf,SAAiB,EACjB,MAAc;IAEd,OAAO,GAAG,OAAO,SAAS,SAAS,SAAS,MAAM,EAAE,CAAC;AACvD,CAAC"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAWA,wBAAsB,GAAG,CACvB,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,EACxB,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,GACvB,OAAO,CAAC,MAAM,CAAC,CAgCjB"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { generateKey } from "./crypto.js";
|
|
2
|
+
import { migrations } from "./sql.js";
|
|
3
|
+
const HELP = `Usage: bexio-oauth <command>
|
|
4
|
+
|
|
5
|
+
Commands:
|
|
6
|
+
sql [--from <n>] Print the schema SQL (all migrations, or from migration <n> on)
|
|
7
|
+
keygen Print a new random 32-byte encryption key (base64url)
|
|
8
|
+
help Show this help
|
|
9
|
+
`;
|
|
10
|
+
export async function run(argv, out, err) {
|
|
11
|
+
const [command, ...rest] = argv;
|
|
12
|
+
switch (command) {
|
|
13
|
+
case "sql": {
|
|
14
|
+
let from = 1;
|
|
15
|
+
if (rest[0] === "--from") {
|
|
16
|
+
from = Number.parseInt(rest[1] ?? "", 10);
|
|
17
|
+
if (!Number.isInteger(from) || from < 1) {
|
|
18
|
+
err("--from needs a migration number, e.g. --from 2\n");
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
else if (rest.length > 0) {
|
|
23
|
+
err(HELP);
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
const selected = (await migrations()).filter((m) => m.number >= from);
|
|
27
|
+
out(selected.map((m) => `-- ${m.name}\n${m.sql.trimEnd()}\n`).join("\n"));
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
case "keygen":
|
|
31
|
+
out(`${generateKey()}\n`);
|
|
32
|
+
return 0;
|
|
33
|
+
case undefined:
|
|
34
|
+
case "help":
|
|
35
|
+
case "--help":
|
|
36
|
+
case "-h":
|
|
37
|
+
out(HELP);
|
|
38
|
+
return 0;
|
|
39
|
+
default:
|
|
40
|
+
err(`Unknown command: ${command}\n\n${HELP}`);
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEtC,MAAM,IAAI,GAAG;;;;;;CAMZ,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,GAAG,CACvB,IAAc,EACd,GAAwB,EACxB,GAAwB;IAExB,MAAM,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IAChC,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,KAAK,CAAC,CAAC,CAAC;YACX,IAAI,IAAI,GAAG,CAAC,CAAC;YACb,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,EAAE,CAAC;gBACzB,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;gBAC1C,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC;oBACxC,GAAG,CAAC,kDAAkD,CAAC,CAAC;oBACxD,OAAO,CAAC,CAAC;gBACX,CAAC;YACH,CAAC;iBAAM,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC3B,GAAG,CAAC,IAAI,CAAC,CAAC;gBACV,OAAO,CAAC,CAAC;YACX,CAAC;YACD,MAAM,QAAQ,GAAG,CAAC,MAAM,UAAU,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC;YACtE,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;YAC1E,OAAO,CAAC,CAAC;QACX,CAAC;QACD,KAAK,QAAQ;YACX,GAAG,CAAC,GAAG,WAAW,EAAE,IAAI,CAAC,CAAC;YAC1B,OAAO,CAAC,CAAC;QACX,KAAK,SAAS,CAAC;QACf,KAAK,MAAM,CAAC;QACZ,KAAK,QAAQ,CAAC;QACd,KAAK,IAAI;YACP,GAAG,CAAC,IAAI,CAAC,CAAC;YACV,OAAO,CAAC,CAAC;QACX;YACE,GAAG,CAAC,oBAAoB,OAAO,OAAO,IAAI,EAAE,CAAC,CAAC;YAC9C,OAAO,CAAC,CAAC;IACb,CAAC;AACH,CAAC"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BexioOAuth, BexioOAuthConfig } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Creates the client. Validates the config up front (throws `invalid_config`);
|
|
4
|
+
* OIDC discovery runs lazily on first use.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createBexioOAuth(config: BexioOAuthConfig): BexioOAuth;
|
|
7
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE/D;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,UAAU,CAYrE"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { resolveConfig } from "./config.js";
|
|
2
|
+
import { callback, handleCallback, start } from "./connect-flow.js";
|
|
3
|
+
import { saveGrant } from "./external-tokens.js";
|
|
4
|
+
import { disconnect, listGrants } from "./grants.js";
|
|
5
|
+
import { refreshIdle } from "./idle-sweep.js";
|
|
6
|
+
import { getAccessToken } from "./refresh.js";
|
|
7
|
+
/**
|
|
8
|
+
* Creates the client. Validates the config up front (throws `invalid_config`);
|
|
9
|
+
* OIDC discovery runs lazily on first use.
|
|
10
|
+
*/
|
|
11
|
+
export function createBexioOAuth(config) {
|
|
12
|
+
const ctx = resolveConfig(config);
|
|
13
|
+
return {
|
|
14
|
+
start: (options) => start(ctx, options),
|
|
15
|
+
callback: (request, options) => callback(ctx, request, options),
|
|
16
|
+
handleCallback: (request, options) => handleCallback(ctx, request, options),
|
|
17
|
+
saveGrant: (input) => saveGrant(ctx, input),
|
|
18
|
+
getAccessToken: (selector) => getAccessToken(ctx, selector),
|
|
19
|
+
listGrants: (query) => listGrants(ctx.store, query),
|
|
20
|
+
disconnect: (selector) => disconnect(ctx, selector),
|
|
21
|
+
refreshIdle: (options) => refreshIdle(ctx, options),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAG9C;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAwB;IACvD,MAAM,GAAG,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IAClC,OAAO;QACL,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC;QACvC,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC;QAC/D,cAAc,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC;QAC3E,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC;QAC3C,cAAc,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,cAAc,CAAC,GAAG,EAAE,QAAQ,CAAC;QAC3D,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC;QACnD,UAAU,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC;QACnD,WAAW,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC;KACpD,CAAC;AACJ,CAAC"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { TokenCipher } from "./crypto.js";
|
|
2
|
+
import { type Oidc } from "./oidc.js";
|
|
3
|
+
import type { GrantStore } from "./store/types.js";
|
|
4
|
+
import type { BexioOAuthConfig } from "./types.js";
|
|
5
|
+
/** The validated configuration and shared services every client method uses. */
|
|
6
|
+
export interface ClientContext {
|
|
7
|
+
clientId: string;
|
|
8
|
+
cipher: TokenCipher;
|
|
9
|
+
store: GrantStore;
|
|
10
|
+
clock: () => Date;
|
|
11
|
+
fetchImpl: typeof fetch;
|
|
12
|
+
oidc: Oidc;
|
|
13
|
+
apiBaseUrl: string;
|
|
14
|
+
redirectUri: URL;
|
|
15
|
+
/** Space-separated, `BASE_SCOPES` included. */
|
|
16
|
+
scope: string;
|
|
17
|
+
lockTtlMs: number;
|
|
18
|
+
defaultReturnTo: string;
|
|
19
|
+
}
|
|
20
|
+
/** Validates the config (throws `invalid_config`) and builds the shared context. */
|
|
21
|
+
export declare function resolveConfig(config: BexioOAuthConfig): ClientContext;
|
|
22
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG1C,OAAO,EAAc,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAElD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,gFAAgF;AAChF,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,UAAU,CAAC;IAClB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,SAAS,EAAE,OAAO,KAAK,CAAC;IACxB,IAAI,EAAE,IAAI,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,GAAG,CAAC;IACjB,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;CACzB;AAYD,oFAAoF;AACpF,wBAAgB,aAAa,CAAC,MAAM,EAAE,gBAAgB,GAAG,aAAa,CAiDrE"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { BASE_SCOPES, BEXIO_API_BASE_URL, BEXIO_ISSUER } from "./claims.js";
|
|
2
|
+
import { TokenCipher } from "./crypto.js";
|
|
3
|
+
import { toMs } from "./duration.js";
|
|
4
|
+
import { BexioOAuthError } from "./errors.js";
|
|
5
|
+
import { createOidc } from "./oidc.js";
|
|
6
|
+
import { safeReturnTo } from "./return-to.js";
|
|
7
|
+
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
8
|
+
/**
|
|
9
|
+
* One refresh makes up to three Bexio calls inside the lock: token (10 s), the
|
|
10
|
+
* JWKS fetch for the refreshed ID token (10 s), and the role (5 s).
|
|
11
|
+
*/
|
|
12
|
+
const MIN_LOCK_TTL_MS = 30_000;
|
|
13
|
+
const isLoopbackHttp = (url) => url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname);
|
|
14
|
+
/** Validates the config (throws `invalid_config`) and builds the shared context. */
|
|
15
|
+
export function resolveConfig(config) {
|
|
16
|
+
for (const key of ["clientId", "clientSecret", "redirectUri"]) {
|
|
17
|
+
if (!config[key])
|
|
18
|
+
throw new BexioOAuthError("invalid_config", `${key} is required`);
|
|
19
|
+
}
|
|
20
|
+
if (!config.store)
|
|
21
|
+
throw new BexioOAuthError("invalid_config", "store is required");
|
|
22
|
+
const cipher = new TokenCipher(config.keys);
|
|
23
|
+
const fetchImpl = config.fetch ?? fetch;
|
|
24
|
+
const issuer = config.issuer ?? BEXIO_ISSUER;
|
|
25
|
+
const redirectUri = new URL(config.redirectUri);
|
|
26
|
+
// Plain http only for local development: the flow cookie would otherwise
|
|
27
|
+
// travel without `Secure`.
|
|
28
|
+
if (redirectUri.protocol !== "https:" && !isLoopbackHttp(redirectUri)) {
|
|
29
|
+
throw new BexioOAuthError("invalid_config", "redirectUri must use https (http is allowed for loopback hosts only)");
|
|
30
|
+
}
|
|
31
|
+
const lockTtlMs = toMs(config.refreshLockTtl ?? "30s", "refreshLockTtl");
|
|
32
|
+
if (lockTtlMs < MIN_LOCK_TTL_MS) {
|
|
33
|
+
throw new BexioOAuthError("invalid_config", `refreshLockTtl must be at least ${MIN_LOCK_TTL_MS / 1000}s`);
|
|
34
|
+
}
|
|
35
|
+
const defaultReturnTo = safeReturnTo(config.defaultReturnTo, "/");
|
|
36
|
+
return {
|
|
37
|
+
clientId: config.clientId,
|
|
38
|
+
cipher,
|
|
39
|
+
store: config.store,
|
|
40
|
+
clock: config.clock ?? (() => new Date()),
|
|
41
|
+
fetchImpl,
|
|
42
|
+
oidc: createOidc({
|
|
43
|
+
issuer,
|
|
44
|
+
clientId: config.clientId,
|
|
45
|
+
clientSecret: config.clientSecret,
|
|
46
|
+
// http is allowed only for local mocks; real issuers must use https.
|
|
47
|
+
allowInsecure: isLoopbackHttp(new URL(issuer)),
|
|
48
|
+
fetchImpl,
|
|
49
|
+
}),
|
|
50
|
+
apiBaseUrl: config.apiBaseUrl ?? BEXIO_API_BASE_URL,
|
|
51
|
+
redirectUri,
|
|
52
|
+
scope: [...new Set([...BASE_SCOPES, ...(config.scopes ?? [])])].join(" "),
|
|
53
|
+
lockTtlMs,
|
|
54
|
+
defaultReturnTo,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ClientContext } from "./config.js";
|
|
2
|
+
import type { CallbackOptions, CallbackResult } from "./types.js";
|
|
3
|
+
/** `BexioOAuth.start`. */
|
|
4
|
+
export declare function start(ctx: ClientContext, options: {
|
|
5
|
+
ownerId: string;
|
|
6
|
+
returnTo?: string;
|
|
7
|
+
}): Promise<Response>;
|
|
8
|
+
/** `BexioOAuth.handleCallback`. */
|
|
9
|
+
export declare function handleCallback(ctx: ClientContext, request: Request, options: CallbackOptions): Promise<CallbackResult>;
|
|
10
|
+
/**
|
|
11
|
+
* `BexioOAuth.callback`: reads the flow cookie once, so a failure can still
|
|
12
|
+
* redirect to the `returnTo` it recorded.
|
|
13
|
+
*/
|
|
14
|
+
export declare function callback(ctx: ClientContext, request: Request, options: CallbackOptions): Promise<Response>;
|
|
15
|
+
//# sourceMappingURL=connect-flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect-flow.d.ts","sourceRoot":"","sources":["../src/connect-flow.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAajD,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAElE,0BAA0B;AAC1B,wBAAsB,KAAK,CACzB,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAC9C,OAAO,CAAC,QAAQ,CAAC,CA8BnB;AAED,mCAAmC;AACnC,wBAAsB,cAAc,CAClC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,cAAc,CAAC,CAQzB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAC5B,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,QAAQ,CAAC,CAkBnB"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as oidc from "openid-client";
|
|
2
|
+
import { BexioOAuthError, isBexioOAuthError } from "./errors.js";
|
|
3
|
+
import { FLOW_TTL_SECONDS, flowCookie, readFlow, sealFlow, } from "./flow-cookie.js";
|
|
4
|
+
import { decodeIfJwt, saveTokens, toGrant, withUserInfo } from "./grants.js";
|
|
5
|
+
import { describe } from "./oidc.js";
|
|
6
|
+
import { tokenExpiry } from "./refresh.js";
|
|
7
|
+
import { safeReturnTo } from "./return-to.js";
|
|
8
|
+
/** `BexioOAuth.start`. */
|
|
9
|
+
export async function start(ctx, options) {
|
|
10
|
+
if (!options?.ownerId)
|
|
11
|
+
throw new BexioOAuthError("invalid_config", "ownerId is required");
|
|
12
|
+
const returnTo = safeReturnTo(options.returnTo, ctx.defaultReturnTo);
|
|
13
|
+
const c = await ctx.oidc.config();
|
|
14
|
+
const verifier = oidc.randomPKCECodeVerifier();
|
|
15
|
+
const state = oidc.randomState();
|
|
16
|
+
const nonce = oidc.randomNonce();
|
|
17
|
+
const url = oidc.buildAuthorizationUrl(c, {
|
|
18
|
+
redirect_uri: ctx.redirectUri.href,
|
|
19
|
+
scope: ctx.scope,
|
|
20
|
+
code_challenge: await oidc.calculatePKCECodeChallenge(verifier),
|
|
21
|
+
code_challenge_method: "S256",
|
|
22
|
+
state,
|
|
23
|
+
nonce,
|
|
24
|
+
});
|
|
25
|
+
const sealed = await sealFlow(ctx.cipher, { s: state, v: verifier, n: nonce, o: options.ownerId, r: returnTo }, ctx.clock());
|
|
26
|
+
const headers = new Headers({
|
|
27
|
+
Location: url.href,
|
|
28
|
+
"Cache-Control": "no-store",
|
|
29
|
+
});
|
|
30
|
+
headers.append("Set-Cookie", flowCookie(ctx.redirectUri, sealed, FLOW_TTL_SECONDS));
|
|
31
|
+
return new Response(null, { status: 302, headers });
|
|
32
|
+
}
|
|
33
|
+
/** `BexioOAuth.handleCallback`. */
|
|
34
|
+
export async function handleCallback(ctx, request, options) {
|
|
35
|
+
assertCallbackOptions(options);
|
|
36
|
+
return completeCallback(ctx, request, options, await readFlow(ctx.cipher, request, ctx.clock()));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* `BexioOAuth.callback`: reads the flow cookie once, so a failure can still
|
|
40
|
+
* redirect to the `returnTo` it recorded.
|
|
41
|
+
*/
|
|
42
|
+
export async function callback(ctx, request, options) {
|
|
43
|
+
const headers = new Headers({ "Cache-Control": "no-store" });
|
|
44
|
+
headers.append("Set-Cookie", flowCookie(ctx.redirectUri, "", 0));
|
|
45
|
+
const origin = ctx.redirectUri.origin;
|
|
46
|
+
assertCallbackOptions(options);
|
|
47
|
+
const flow = await readFlow(ctx.cipher, request, ctx.clock());
|
|
48
|
+
let location;
|
|
49
|
+
try {
|
|
50
|
+
const result = await completeCallback(ctx, request, options, flow);
|
|
51
|
+
location = new URL(result.returnTo, origin);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (!isBexioOAuthError(error) || error.code === "invalid_config")
|
|
55
|
+
throw error;
|
|
56
|
+
location = new URL(flow?.r ?? ctx.defaultReturnTo, origin);
|
|
57
|
+
location.searchParams.set("bexio_error", error.code);
|
|
58
|
+
}
|
|
59
|
+
headers.set("Location", location.href);
|
|
60
|
+
return new Response(null, { status: 302, headers });
|
|
61
|
+
}
|
|
62
|
+
function assertCallbackOptions(options) {
|
|
63
|
+
if (!options ||
|
|
64
|
+
("ownerId" in options
|
|
65
|
+
? !options.ownerId
|
|
66
|
+
: typeof options.authorizeOwner !== "function")) {
|
|
67
|
+
throw new BexioOAuthError("invalid_config", "pass ownerId or authorizeOwner");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** The callback after the flow cookie was read (`flow` is null when unreadable). */
|
|
71
|
+
async function completeCallback(ctx, request, options, flow) {
|
|
72
|
+
const params = new URL(request.url).searchParams;
|
|
73
|
+
if (!flow || params.get("state") !== flow.s) {
|
|
74
|
+
throw new BexioOAuthError("state_mismatch", "flow cookie missing or expired, or state mismatch");
|
|
75
|
+
}
|
|
76
|
+
const ownerAllowed = "ownerId" in options
|
|
77
|
+
? flow.o === options.ownerId
|
|
78
|
+
: (await options.authorizeOwner(flow.o)) === true;
|
|
79
|
+
if (!ownerAllowed) {
|
|
80
|
+
throw new BexioOAuthError("owner_mismatch", "the signed-in user may not connect Bexio for the owner used at start");
|
|
81
|
+
}
|
|
82
|
+
const error = params.get("error");
|
|
83
|
+
if (error) {
|
|
84
|
+
throw new BexioOAuthError(error === "access_denied" ? "access_denied" : "idp_error", `Bexio returned ${error}`);
|
|
85
|
+
}
|
|
86
|
+
// Rebuild the URL on the registered redirect URI, so a proxy that rewrites
|
|
87
|
+
// the host doesn't change the redirect_uri sent to the token endpoint.
|
|
88
|
+
const currentUrl = new URL(ctx.redirectUri.href);
|
|
89
|
+
currentUrl.search = params.toString();
|
|
90
|
+
const c = await ctx.oidc.config();
|
|
91
|
+
let tokens;
|
|
92
|
+
try {
|
|
93
|
+
tokens = await oidc.authorizationCodeGrant(c, currentUrl, {
|
|
94
|
+
pkceCodeVerifier: flow.v,
|
|
95
|
+
expectedState: flow.s,
|
|
96
|
+
expectedNonce: flow.n,
|
|
97
|
+
idTokenExpected: true,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (cause) {
|
|
101
|
+
if (cause instanceof oidc.AuthorizationResponseError) {
|
|
102
|
+
throw new BexioOAuthError(cause.error === "access_denied" ? "access_denied" : "idp_error", `Bexio returned ${cause.error}`, { cause: describe(cause) });
|
|
103
|
+
}
|
|
104
|
+
if (cause instanceof TypeError) {
|
|
105
|
+
throw new BexioOAuthError("idp_error", "Bexio token endpoint unreachable", { cause: describe(cause) });
|
|
106
|
+
}
|
|
107
|
+
throw new BexioOAuthError("invalid_callback", "authorization code exchange or ID token validation failed", { cause: describe(cause) });
|
|
108
|
+
}
|
|
109
|
+
const now = ctx.clock();
|
|
110
|
+
const claims = await withUserInfo(ctx, { ...decodeIfJwt(tokens.access_token), ...(tokens.claims() ?? {}) }, tokens.access_token);
|
|
111
|
+
const record = await saveTokens(ctx, {
|
|
112
|
+
ownerId: flow.o,
|
|
113
|
+
claims,
|
|
114
|
+
accessToken: tokens.access_token,
|
|
115
|
+
refreshToken: tokens.refresh_token ?? null,
|
|
116
|
+
...tokenExpiry(tokens, now),
|
|
117
|
+
scope: tokens.scope ?? null,
|
|
118
|
+
});
|
|
119
|
+
const headers = new Headers();
|
|
120
|
+
headers.append("Set-Cookie", flowCookie(ctx.redirectUri, "", 0));
|
|
121
|
+
return { grant: toGrant(record), returnTo: flow.r, headers };
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=connect-flow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect-flow.js","sourceRoot":"","sources":["../src/connect-flow.ts"],"names":[],"mappings":"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"}
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { JWTPayload } from "jose";
|
|
2
|
+
/**
|
|
3
|
+
* Encryption keys, by key id. `current` is used for new writes; every key in
|
|
4
|
+
* `keys` can still decrypt. Keys are 32 random bytes, base64url-encoded
|
|
5
|
+
* (`npx @soulcode/bexio-oauth keygen`).
|
|
6
|
+
*/
|
|
7
|
+
export interface Keyring {
|
|
8
|
+
current: string;
|
|
9
|
+
keys: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
/** A new random key for the keyring, base64url-encoded. */
|
|
12
|
+
export declare function generateKey(): string;
|
|
13
|
+
/**
|
|
14
|
+
* Encrypts tokens at rest as compact JWE (`dir` + `A256GCM`) and seals the
|
|
15
|
+
* short-lived flow cookie. The `bxg` protected header binds a token to the
|
|
16
|
+
* grant it belongs to, so ciphertext copied to another row fails to decrypt.
|
|
17
|
+
*/
|
|
18
|
+
export declare class TokenCipher {
|
|
19
|
+
#private;
|
|
20
|
+
constructor(keyring: Keyring);
|
|
21
|
+
encrypt(plaintext: string, binding: string): Promise<string>;
|
|
22
|
+
decrypt(jwe: string, binding: string): Promise<string>;
|
|
23
|
+
seal(payload: JWTPayload, ttlSeconds: number, now: Date): Promise<string>;
|
|
24
|
+
/** Returns the payload, or `null` when the value is invalid or expired. */
|
|
25
|
+
unseal(token: string, now: Date): Promise<JWTPayload | null>;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAA8B,UAAU,EAAE,MAAM,MAAM,CAAC;AAGnE;;;;GAIG;AACH,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAUD,2DAA2D;AAC3D,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAMD;;;;GAIG;AACH,qBAAa,WAAW;;gBAKV,OAAO,EAAE,OAAO;IA6BtB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAW5D,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmBtD,IAAI,CACR,OAAO,EAAE,UAAU,EACnB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,IAAI,GACR,OAAO,CAAC,MAAM,CAAC;IASlB,2EAA2E;IACrE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;CAyCnE"}
|