@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.
Files changed (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +66 -2
  3. package/dist/bin.d.ts +3 -0
  4. package/dist/bin.d.ts.map +1 -0
  5. package/dist/bin.js +4 -0
  6. package/dist/bin.js.map +1 -0
  7. package/dist/claims.d.ts +31 -0
  8. package/dist/claims.d.ts.map +1 -0
  9. package/dist/claims.js +58 -0
  10. package/dist/claims.js.map +1 -0
  11. package/dist/cli.d.ts +2 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +44 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/client.d.ts +7 -0
  16. package/dist/client.d.ts.map +1 -0
  17. package/dist/client.js +24 -0
  18. package/dist/client.js.map +1 -0
  19. package/dist/config.d.ts +22 -0
  20. package/dist/config.d.ts.map +1 -0
  21. package/dist/config.js +57 -0
  22. package/dist/config.js.map +1 -0
  23. package/dist/connect-flow.d.ts +15 -0
  24. package/dist/connect-flow.d.ts.map +1 -0
  25. package/dist/connect-flow.js +123 -0
  26. package/dist/connect-flow.js.map +1 -0
  27. package/dist/crypto.d.ts +27 -0
  28. package/dist/crypto.d.ts.map +1 -0
  29. package/dist/crypto.js +106 -0
  30. package/dist/crypto.js.map +1 -0
  31. package/dist/duration.d.ts +4 -0
  32. package/dist/duration.d.ts.map +1 -0
  33. package/dist/duration.js +15 -0
  34. package/dist/duration.js.map +1 -0
  35. package/dist/errors.d.ts +38 -0
  36. package/dist/errors.d.ts.map +1 -0
  37. package/dist/errors.js +13 -0
  38. package/dist/errors.js.map +1 -0
  39. package/dist/external-tokens.d.ts +8 -0
  40. package/dist/external-tokens.d.ts.map +1 -0
  41. package/dist/external-tokens.js +73 -0
  42. package/dist/external-tokens.js.map +1 -0
  43. package/dist/flow-cookie.d.ts +23 -0
  44. package/dist/flow-cookie.d.ts.map +1 -0
  45. package/dist/flow-cookie.js +44 -0
  46. package/dist/flow-cookie.js.map +1 -0
  47. package/dist/grants.d.ts +36 -0
  48. package/dist/grants.d.ts.map +1 -0
  49. package/dist/grants.js +115 -0
  50. package/dist/grants.js.map +1 -0
  51. package/dist/idle-sweep.d.ts +11 -0
  52. package/dist/idle-sweep.d.ts.map +1 -0
  53. package/dist/idle-sweep.js +70 -0
  54. package/dist/idle-sweep.js.map +1 -0
  55. package/dist/index.d.ts +12 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +9 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/migrations.generated.d.ts +3 -0
  60. package/dist/migrations.generated.d.ts.map +1 -0
  61. package/dist/migrations.generated.js +40 -0
  62. package/dist/migrations.generated.js.map +1 -0
  63. package/dist/oidc.d.ts +30 -0
  64. package/dist/oidc.d.ts.map +1 -0
  65. package/dist/oidc.js +67 -0
  66. package/dist/oidc.js.map +1 -0
  67. package/dist/permissions.d.ts +9 -0
  68. package/dist/permissions.d.ts.map +1 -0
  69. package/dist/permissions.js +29 -0
  70. package/dist/permissions.js.map +1 -0
  71. package/dist/refresh.d.ts +30 -0
  72. package/dist/refresh.d.ts.map +1 -0
  73. package/dist/refresh.js +139 -0
  74. package/dist/refresh.js.map +1 -0
  75. package/dist/return-to.d.ts +8 -0
  76. package/dist/return-to.d.ts.map +1 -0
  77. package/dist/return-to.js +31 -0
  78. package/dist/return-to.js.map +1 -0
  79. package/dist/sql.d.ts +14 -0
  80. package/dist/sql.d.ts.map +1 -0
  81. package/dist/sql.js +11 -0
  82. package/dist/sql.js.map +1 -0
  83. package/dist/store/memory.d.ts +7 -0
  84. package/dist/store/memory.d.ts.map +1 -0
  85. package/dist/store/memory.js +104 -0
  86. package/dist/store/memory.js.map +1 -0
  87. package/dist/store/postgres.d.ts +17 -0
  88. package/dist/store/postgres.d.ts.map +1 -0
  89. package/dist/store/postgres.js +177 -0
  90. package/dist/store/postgres.js.map +1 -0
  91. package/dist/store/types.d.ts +105 -0
  92. package/dist/store/types.d.ts.map +1 -0
  93. package/dist/store/types.js +2 -0
  94. package/dist/store/types.js.map +1 -0
  95. package/dist/types.d.ts +150 -0
  96. package/dist/types.d.ts.map +1 -0
  97. package/dist/types.js +2 -0
  98. package/dist/types.js.map +1 -0
  99. package/package.json +60 -4
  100. 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
- # Temporary Holding Version
1
+ # @soulcode/bexio-oauth
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
package/dist/bin.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "./cli.js";
3
+ process.exitCode = await run(process.argv.slice(2), (s) => process.stdout.write(s), (s) => process.stderr.write(s));
4
+ //# sourceMappingURL=bin.js.map
@@ -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"}
@@ -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,2 @@
1
+ export declare function run(argv: string[], out: (s: string) => void, err: (s: string) => void): Promise<number>;
2
+ //# sourceMappingURL=cli.d.ts.map
@@ -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
@@ -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"}
@@ -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"}
@@ -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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"}
@@ -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"}