@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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.js","sourceRoot":"","sources":["../../src/store/memory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { GrantStore } from "./types.js";
2
+ /**
3
+ * The only thing the Postgres store needs from a driver. A `pg` `Pool` or
4
+ * `Client` matches it as is; other drivers need a few lines of wrapper.
5
+ */
6
+ export interface SqlClient {
7
+ query(text: string, params?: unknown[]): Promise<{
8
+ rows: unknown[];
9
+ }>;
10
+ }
11
+ /**
12
+ * Grant store on the `bexio_grants` table (create it with
13
+ * `npx @soulcode/bexio-oauth sql`). Every operation is a single statement,
14
+ * so it works through transaction-mode poolers and HTTP drivers.
15
+ */
16
+ export declare function postgresStore(sql: SqlClient): GrantStore;
17
+ //# sourceMappingURL=postgres.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../src/store/postgres.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAe,UAAU,EAAE,MAAM,YAAY,CAAC;AAE1D;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,OAAO,EAAE,CAAA;KAAE,CAAC,CAAC;CACvE;AAoED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,SAAS,GAAG,UAAU,CA0KxD"}
@@ -0,0 +1,177 @@
1
+ import { BexioOAuthError } from "../errors.js";
2
+ // `id` is a uuid column: any other value matches no row, and would make
3
+ // Postgres reject the cast instead of returning nothing.
4
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5
+ const COLUMNS = `id, owner_id, bexio_company_id, bexio_user_id, bexio_company_user_id,
6
+ company_name, user_name, user_email, role, status, access_token, refresh_token,
7
+ access_expires_at, refresh_expires_at, scope, refresh_locked_until,
8
+ last_refreshed_at, created_at, updated_at`;
9
+ function date(value) {
10
+ if (value === null)
11
+ return null;
12
+ return value instanceof Date ? value : new Date(value);
13
+ }
14
+ function toRecord(row) {
15
+ return {
16
+ id: row.id,
17
+ ownerId: row.owner_id,
18
+ companyId: row.bexio_company_id,
19
+ userId: row.bexio_user_id,
20
+ companyUserId: row.bexio_company_user_id === null
21
+ ? null
22
+ : Number(row.bexio_company_user_id),
23
+ companyName: row.company_name,
24
+ userName: row.user_name,
25
+ userEmail: row.user_email,
26
+ role: row.role,
27
+ status: row.status,
28
+ accessToken: row.access_token,
29
+ refreshToken: row.refresh_token,
30
+ accessExpiresAt: date(row.access_expires_at),
31
+ refreshExpiresAt: date(row.refresh_expires_at),
32
+ scope: row.scope,
33
+ refreshLockedUntil: date(row.refresh_locked_until),
34
+ lastRefreshedAt: date(row.last_refreshed_at),
35
+ createdAt: date(row.created_at),
36
+ updatedAt: date(row.updated_at),
37
+ };
38
+ }
39
+ /**
40
+ * Grant store on the `bexio_grants` table (create it with
41
+ * `npx @soulcode/bexio-oauth sql`). Every operation is a single statement,
42
+ * so it works through transaction-mode poolers and HTTP drivers.
43
+ */
44
+ export function postgresStore(sql) {
45
+ const one = async (text, params) => {
46
+ const { rows } = await sql.query(text, params);
47
+ return rows[0] ? toRecord(rows[0]) : null;
48
+ };
49
+ const many = async (text, params) => {
50
+ const { rows } = await sql.query(text, params);
51
+ return rows.map(toRecord);
52
+ };
53
+ return {
54
+ async upsertGrant(g) {
55
+ const row = await one(`INSERT INTO bexio_grants (owner_id, bexio_company_id, bexio_user_id,
56
+ bexio_company_user_id, company_name, user_name, user_email, role, status,
57
+ access_token, refresh_token, access_expires_at, refresh_expires_at, scope,
58
+ refresh_locked_until, last_refreshed_at, created_at, updated_at)
59
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'active', $9, $10, $11, $12, $13, NULL, $14, $14, $14)
60
+ ON CONFLICT (owner_id, bexio_company_id, bexio_user_id) DO UPDATE SET
61
+ bexio_company_user_id = EXCLUDED.bexio_company_user_id,
62
+ company_name = EXCLUDED.company_name,
63
+ user_name = EXCLUDED.user_name,
64
+ user_email = EXCLUDED.user_email,
65
+ role = EXCLUDED.role,
66
+ status = 'active',
67
+ access_token = EXCLUDED.access_token,
68
+ refresh_token = EXCLUDED.refresh_token,
69
+ access_expires_at = EXCLUDED.access_expires_at,
70
+ refresh_expires_at = EXCLUDED.refresh_expires_at,
71
+ scope = EXCLUDED.scope,
72
+ refresh_locked_until = NULL,
73
+ refresh_lock_token = NULL,
74
+ last_refreshed_at = EXCLUDED.last_refreshed_at,
75
+ updated_at = EXCLUDED.updated_at
76
+ RETURNING ${COLUMNS}`, [
77
+ g.ownerId,
78
+ g.companyId,
79
+ g.userId,
80
+ g.companyUserId,
81
+ g.companyName,
82
+ g.userName,
83
+ g.userEmail,
84
+ g.role,
85
+ g.accessToken,
86
+ g.refreshToken,
87
+ g.accessExpiresAt,
88
+ g.refreshExpiresAt,
89
+ g.scope,
90
+ g.now,
91
+ ]);
92
+ if (!row) {
93
+ throw new BexioOAuthError("invalid_config", "SqlClient returned no row for INSERT … RETURNING; check the driver wrapper");
94
+ }
95
+ return row;
96
+ },
97
+ async findGrants({ ownerId, companyId, grantId }) {
98
+ if (grantId !== undefined && !UUID.test(grantId))
99
+ return [];
100
+ return many(`SELECT ${COLUMNS} FROM bexio_grants
101
+ WHERE owner_id = $1
102
+ AND ($2::text IS NULL OR bexio_company_id = $2)
103
+ AND ($3::uuid IS NULL OR id = $3::uuid)
104
+ ORDER BY created_at, id`, [ownerId, companyId ?? null, grantId ?? null]);
105
+ },
106
+ async getGrant(id) {
107
+ if (!UUID.test(id))
108
+ return null;
109
+ return one(`SELECT ${COLUMNS} FROM bexio_grants WHERE id = $1`, [id]);
110
+ },
111
+ async acquireRefreshLock({ id, now, lockUntil, lockToken, accessExpiresBefore, refreshedBefore, }) {
112
+ return one(`UPDATE bexio_grants SET refresh_locked_until = $3, refresh_lock_token = $6
113
+ WHERE id = $1
114
+ AND status = 'active'
115
+ AND (refresh_locked_until IS NULL OR refresh_locked_until <= $2)
116
+ AND (
117
+ ($4::timestamptz IS NOT NULL AND (access_expires_at IS NULL OR access_expires_at <= $4))
118
+ OR ($5::timestamptz IS NOT NULL AND last_refreshed_at < $5)
119
+ )
120
+ RETURNING ${COLUMNS}`, [
121
+ id,
122
+ now,
123
+ lockUntil,
124
+ accessExpiresBefore ?? null,
125
+ refreshedBefore ?? null,
126
+ lockToken,
127
+ ]);
128
+ },
129
+ // Holder writes are fenced by refresh_lock_token: a holder whose lock
130
+ // expired or was cleared by a reconnect matches no row and changes nothing.
131
+ async saveRefreshed(id, lockToken, r) {
132
+ return one(`UPDATE bexio_grants SET
133
+ access_token = $2,
134
+ refresh_token = COALESCE($3, refresh_token),
135
+ access_expires_at = $4,
136
+ refresh_expires_at = COALESCE($5, refresh_expires_at),
137
+ scope = COALESCE($6, scope),
138
+ role = COALESCE($7, role),
139
+ status = 'active',
140
+ refresh_locked_until = NULL,
141
+ refresh_lock_token = NULL,
142
+ last_refreshed_at = $8,
143
+ updated_at = $8
144
+ WHERE id = $1 AND refresh_lock_token = $9
145
+ RETURNING ${COLUMNS}`, [
146
+ id,
147
+ r.accessToken,
148
+ r.refreshToken,
149
+ r.accessExpiresAt,
150
+ r.refreshExpiresAt,
151
+ r.scope,
152
+ r.role,
153
+ r.now,
154
+ lockToken,
155
+ ]);
156
+ },
157
+ async markReauthRequired(id, lockToken, now) {
158
+ await sql.query(`UPDATE bexio_grants SET status = 'reauth_required',
159
+ refresh_locked_until = NULL, refresh_lock_token = NULL, updated_at = $2
160
+ WHERE id = $1 AND refresh_lock_token = $3`, [id, now, lockToken]);
161
+ },
162
+ async releaseRefreshLock(id, lockToken) {
163
+ await sql.query(`UPDATE bexio_grants SET refresh_locked_until = NULL, refresh_lock_token = NULL
164
+ WHERE id = $1 AND refresh_lock_token = $2`, [id, lockToken]);
165
+ },
166
+ async deleteGrant(id) {
167
+ await sql.query(`DELETE FROM bexio_grants WHERE id = $1`, [id]);
168
+ },
169
+ async listIdleGrants({ refreshedBefore, limit }) {
170
+ return many(`SELECT ${COLUMNS} FROM bexio_grants
171
+ WHERE status = 'active' AND last_refreshed_at < $1
172
+ ORDER BY last_refreshed_at
173
+ LIMIT $2`, [refreshedBefore, limit]);
174
+ },
175
+ };
176
+ }
177
+ //# sourceMappingURL=postgres.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../src/store/postgres.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,105 @@
1
+ import type { Role } from "../permissions.js";
2
+ export type GrantStatus = "active" | "reauth_required";
3
+ /** A stored grant. Token fields hold JWE ciphertext, never plaintext. */
4
+ export interface GrantRecord {
5
+ id: string;
6
+ ownerId: string;
7
+ companyId: string;
8
+ userId: string;
9
+ companyUserId: number | null;
10
+ companyName: string | null;
11
+ userName: string | null;
12
+ userEmail: string | null;
13
+ role: Role;
14
+ status: GrantStatus;
15
+ accessToken: string;
16
+ refreshToken: string | null;
17
+ accessExpiresAt: Date | null;
18
+ refreshExpiresAt: Date | null;
19
+ scope: string | null;
20
+ refreshLockedUntil: Date | null;
21
+ lastRefreshedAt: Date;
22
+ createdAt: Date;
23
+ updatedAt: Date;
24
+ }
25
+ /** Insert, or overwrite the tokens of, the grant for (owner, company, user). */
26
+ export interface GrantUpsert {
27
+ ownerId: string;
28
+ companyId: string;
29
+ userId: string;
30
+ companyUserId: number | null;
31
+ companyName: string | null;
32
+ userName: string | null;
33
+ userEmail: string | null;
34
+ role: Role;
35
+ accessToken: string;
36
+ refreshToken: string | null;
37
+ accessExpiresAt: Date | null;
38
+ refreshExpiresAt: Date | null;
39
+ scope: string | null;
40
+ now: Date;
41
+ }
42
+ export interface GrantRefresh {
43
+ accessToken: string;
44
+ /** `null` keeps the stored refresh token (the IdP didn't rotate it). */
45
+ refreshToken: string | null;
46
+ accessExpiresAt: Date | null;
47
+ /** `null` keeps the stored value. */
48
+ refreshExpiresAt: Date | null;
49
+ /** `null` keeps the stored value. */
50
+ scope: string | null;
51
+ /** `null` keeps the stored role. */
52
+ role: Role | null;
53
+ now: Date;
54
+ }
55
+ export interface LockRequest {
56
+ id: string;
57
+ now: Date;
58
+ lockUntil: Date;
59
+ /**
60
+ * Random value identifying this lock holder. Writes that need the lock pass
61
+ * it back, so a holder whose lock expired (or was cleared by a reconnect)
62
+ * can't overwrite newer data.
63
+ */
64
+ lockToken: string;
65
+ /** Lock only if the access token expires before this instant (lazy refresh)… */
66
+ accessExpiresBefore?: Date;
67
+ /** …or only if the grant was last refreshed before this instant (idle sweep). */
68
+ refreshedBefore?: Date;
69
+ }
70
+ /**
71
+ * Persistence for grants. Every method must be atomic on its own: the
72
+ * library never needs multi-statement transactions.
73
+ */
74
+ export interface GrantStore {
75
+ upsertGrant(input: GrantUpsert): Promise<GrantRecord>;
76
+ findGrants(query: {
77
+ ownerId: string;
78
+ companyId?: string;
79
+ grantId?: string;
80
+ }): Promise<GrantRecord[]>;
81
+ getGrant(id: string): Promise<GrantRecord | null>;
82
+ /**
83
+ * Takes the refresh lock if the grant is active, not locked (or the lock
84
+ * expired), and still matches the `accessExpiresBefore` / `refreshedBefore`
85
+ * condition. Returns the locked row, or `null` if someone else holds the
86
+ * lock or the grant no longer needs a refresh.
87
+ */
88
+ acquireRefreshLock(request: LockRequest): Promise<GrantRecord | null>;
89
+ /**
90
+ * Stores refreshed tokens, sets `lastRefreshedAt`, and releases the lock.
91
+ * Returns `null` (and changes nothing) unless `lockToken` still holds it.
92
+ */
93
+ saveRefreshed(id: string, lockToken: string, refresh: GrantRefresh): Promise<GrantRecord | null>;
94
+ /** Sets `status = reauth_required` and releases the lock, if `lockToken` holds it. */
95
+ markReauthRequired(id: string, lockToken: string, now: Date): Promise<void>;
96
+ /** Releases the lock, if `lockToken` holds it. */
97
+ releaseRefreshLock(id: string, lockToken: string): Promise<void>;
98
+ deleteGrant(id: string): Promise<void>;
99
+ /** Active grants last refreshed before `refreshedBefore`, oldest first. */
100
+ listIdleGrants(query: {
101
+ refreshedBefore: Date;
102
+ limit: number;
103
+ }): Promise<GrantRecord[]>;
104
+ }
105
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,150 @@
1
+ import type { Keyring } from "./crypto.js";
2
+ import type { Duration } from "./duration.js";
3
+ import type { Role } from "./permissions.js";
4
+ import type { GrantStatus, GrantStore } from "./store/types.js";
5
+ export interface BexioOAuthConfig {
6
+ /** From your Bexio OAuth app. */
7
+ clientId: string;
8
+ clientSecret: string;
9
+ /** Must exactly match a redirect URI registered at Bexio. */
10
+ redirectUri: string;
11
+ /** Bexio API scopes your app needs; `BASE_SCOPES` are always added. */
12
+ scopes?: string[];
13
+ /** Encryption keyring (see `generateKey`). */
14
+ keys: Keyring;
15
+ store: GrantStore;
16
+ /** Defaults to Bexio's production issuer. Override only to point at a mock. */
17
+ issuer?: string;
18
+ /** Defaults to `https://api.bexio.com`. Override only to point at a mock. */
19
+ apiBaseUrl?: string;
20
+ /** How long one refresh may hold a grant's lock. Default `"30s"`. */
21
+ refreshLockTtl?: Duration;
22
+ /** Where `callback` redirects when the flow cookie is gone. Default `"/"`. */
23
+ defaultReturnTo?: string;
24
+ /** Custom `fetch` for every outgoing request (tests, proxies). */
25
+ fetch?: typeof fetch;
26
+ /** Clock override for tests. */
27
+ clock?: () => Date;
28
+ }
29
+ /** A grant as the app sees it: no token material. */
30
+ export interface Grant {
31
+ id: string;
32
+ ownerId: string;
33
+ companyId: string;
34
+ companyName: string | null;
35
+ userId: string;
36
+ userName: string | null;
37
+ userEmail: string | null;
38
+ companyUserId: number | null;
39
+ role: Role;
40
+ status: GrantStatus;
41
+ scope: string | null;
42
+ accessExpiresAt: Date | null;
43
+ lastRefreshedAt: Date;
44
+ createdAt: Date;
45
+ updatedAt: Date;
46
+ }
47
+ /** Identifies one grant of an owner. */
48
+ export interface GrantSelector {
49
+ ownerId: string;
50
+ companyId?: string;
51
+ grantId?: string;
52
+ }
53
+ export interface AccessToken {
54
+ accessToken: string;
55
+ expiresAt: Date | null;
56
+ grant: Grant;
57
+ }
58
+ /** Tokens obtained by your auth framework, for `saveGrant`. */
59
+ export interface ExternalTokens {
60
+ accessToken: string;
61
+ refreshToken?: string | null;
62
+ idToken?: string | null;
63
+ /** Access-token expiry; defaults to the access token's `exp` claim. */
64
+ expiresAt?: Date | null;
65
+ scope?: string | null;
66
+ }
67
+ export interface RefreshIdleResult {
68
+ refreshed: number;
69
+ /** Idle grants another process was refreshing at the same time. */
70
+ skipped: number;
71
+ reauthRequired: number;
72
+ failed: number;
73
+ /**
74
+ * Idle grants not attempted because `timeBudget` ran out (0 when every idle
75
+ * grant of this run was attempted). The next run picks them up first.
76
+ */
77
+ remaining: number;
78
+ }
79
+ /**
80
+ * How the callback checks the owner. Either the `ownerId` your app derives
81
+ * from the signed-in session (must equal the one passed to `start`), or
82
+ * `authorizeOwner`, which receives the owner recorded at `start` and returns
83
+ * whether the signed-in user may connect Bexio for it. Use the latter when
84
+ * one callback URL serves many owners (e.g. workspaces).
85
+ */
86
+ export type CallbackOptions = {
87
+ ownerId: string;
88
+ } | {
89
+ authorizeOwner: (ownerId: string) => boolean | Promise<boolean>;
90
+ };
91
+ export interface CallbackResult {
92
+ grant: Grant;
93
+ returnTo: string;
94
+ /** Contains the `Set-Cookie` that deletes the flow cookie; copy it onto your response. */
95
+ headers: Headers;
96
+ }
97
+ /** The client returned by `createBexioOAuth`. */
98
+ export interface BexioOAuth {
99
+ /**
100
+ * Starts the Connect flow: returns a redirect to Bexio that sets the
101
+ * encrypted flow cookie. Derive `ownerId` from your authenticated session.
102
+ * `returnTo` must be a relative path (default: `defaultReturnTo`).
103
+ */
104
+ start(options: {
105
+ ownerId: string;
106
+ returnTo?: string;
107
+ }): Promise<Response>;
108
+ /**
109
+ * Handles the redirect back from Bexio, saves the grant, and redirects to
110
+ * `returnTo` (or `returnTo?bexio_error=<code>` on failure). Only
111
+ * `invalid_config` and non-library errors are thrown.
112
+ */
113
+ callback(request: Request, options: CallbackOptions): Promise<Response>;
114
+ /**
115
+ * Handles the redirect back from Bexio and saves the grant. Throws a
116
+ * `BexioOAuthError` on failure; use `callback` for a ready-made redirect.
117
+ */
118
+ handleCallback(request: Request, options: CallbackOptions): Promise<CallbackResult>;
119
+ /**
120
+ * Saves tokens obtained by your auth framework ("Sign in with Bexio").
121
+ * Verifies the tokens' signatures against Bexio's keys first.
122
+ */
123
+ saveGrant(input: {
124
+ ownerId: string;
125
+ tokens: ExternalTokens;
126
+ }): Promise<Grant>;
127
+ /**
128
+ * A valid access token for the owner's grant, refreshed when needed.
129
+ * Safe to call from background jobs and from many instances at once.
130
+ */
131
+ getAccessToken(selector: GrantSelector): Promise<AccessToken>;
132
+ /** The owner's grants, without token material. */
133
+ listGrants(query: {
134
+ ownerId: string;
135
+ }): Promise<Grant[]>;
136
+ /** Revokes the refresh token at Bexio (best effort) and deletes the grant. */
137
+ disconnect(selector: GrantSelector): Promise<void>;
138
+ /**
139
+ * Refreshes active grants not refreshed for `idleFor`, so Bexio doesn't end
140
+ * their offline sessions. Run it on a schedule. `limit` (default 100) caps
141
+ * the grants one run looks at; `timeBudget` stops the run before the next
142
+ * grant once that much time has passed.
143
+ */
144
+ refreshIdle(options: {
145
+ idleFor: Duration;
146
+ limit?: number;
147
+ timeBudget?: Duration;
148
+ }): Promise<RefreshIdleResult>;
149
+ }
150
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,62 @@
1
1
  {
2
2
  "name": "@soulcode/bexio-oauth",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Bexio OAuth token custody for Node apps: connect Bexio accounts, store tokens encrypted in Postgres, get a valid access token anywhere.",
5
+ "keywords": [
6
+ "bexio",
7
+ "oauth",
8
+ "oidc",
9
+ "openid-connect",
10
+ "postgres",
11
+ "token"
12
+ ],
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/SoulcodeAgency/bexio-oauth.git",
17
+ "directory": "packages/bexio-oauth"
18
+ },
19
+ "homepage": "https://github.com/SoulcodeAgency/bexio-oauth/blob/main/docs/integration.md",
20
+ "bugs": "https://github.com/SoulcodeAgency/bexio-oauth/issues",
21
+ "type": "module",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js"
26
+ },
27
+ "./package.json": "./package.json"
28
+ },
29
+ "bin": {
30
+ "bexio-oauth": "./dist/bin.js"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "sql"
35
+ ],
36
+ "sideEffects": false,
37
+ "engines": {
38
+ "node": ">=24"
39
+ },
40
+ "scripts": {
41
+ "sql:generate": "node ../../scripts/generate-migrations.mjs",
42
+ "prebuild": "node ../../scripts/generate-migrations.mjs --check",
43
+ "build": "tsc -p tsconfig.build.json",
44
+ "typecheck": "tsc -p tsconfig.json --noEmit",
45
+ "test": "vitest run",
46
+ "test:watch": "vitest",
47
+ "prepack": "npm run build"
48
+ },
49
+ "dependencies": {
50
+ "jose": "^6.2.13",
51
+ "openid-client": "^6.8.8"
52
+ },
53
+ "devDependencies": {
54
+ "@types/pg": "8.23.1",
55
+ "mock-bexio": "*",
56
+ "pg": "8.23.1",
57
+ "vitest": "5.0.5"
58
+ },
59
+ "publishConfig": {
60
+ "access": "public"
61
+ }
62
+ }
@@ -0,0 +1,32 @@
1
+ -- @soulcode/bexio-oauth 0001: the grants table.
2
+ -- One row per (owner, Bexio company, Bexio user). Token columns hold JWE
3
+ -- ciphertext; only your server-side database role should be able to read them.
4
+
5
+ CREATE TABLE IF NOT EXISTS bexio_grants (
6
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
7
+ owner_id text NOT NULL,
8
+ bexio_company_id text NOT NULL,
9
+ bexio_user_id text NOT NULL,
10
+ bexio_company_user_id integer,
11
+ company_name text,
12
+ user_name text,
13
+ user_email text,
14
+ role text NOT NULL DEFAULT 'member' CHECK (role IN ('admin', 'member')),
15
+ status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'reauth_required')),
16
+ access_token text NOT NULL,
17
+ refresh_token text,
18
+ access_expires_at timestamptz,
19
+ refresh_expires_at timestamptz,
20
+ scope text,
21
+ refresh_locked_until timestamptz,
22
+ refresh_lock_token text,
23
+ last_refreshed_at timestamptz NOT NULL DEFAULT now(),
24
+ created_at timestamptz NOT NULL DEFAULT now(),
25
+ updated_at timestamptz NOT NULL DEFAULT now(),
26
+ CONSTRAINT bexio_grants_owner_company_user_key UNIQUE (owner_id, bexio_company_id, bexio_user_id)
27
+ );
28
+
29
+ -- refreshIdle(): active grants by last refresh.
30
+ CREATE INDEX IF NOT EXISTS bexio_grants_idle_idx
31
+ ON bexio_grants (last_refreshed_at)
32
+ WHERE status = 'active';