@soulcode/bexio-oauth 0.0.0-stage → 0.1.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 (56) 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 +140 -0
  16. package/dist/client.d.ts.map +1 -0
  17. package/dist/client.js +635 -0
  18. package/dist/client.js.map +1 -0
  19. package/dist/crypto.d.ts +27 -0
  20. package/dist/crypto.d.ts.map +1 -0
  21. package/dist/crypto.js +101 -0
  22. package/dist/crypto.js.map +1 -0
  23. package/dist/duration.d.ts +4 -0
  24. package/dist/duration.d.ts.map +1 -0
  25. package/dist/duration.js +15 -0
  26. package/dist/duration.js.map +1 -0
  27. package/dist/errors.d.ts +38 -0
  28. package/dist/errors.d.ts.map +1 -0
  29. package/dist/errors.js +13 -0
  30. package/dist/errors.js.map +1 -0
  31. package/dist/index.d.ts +11 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +9 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/permissions.d.ts +9 -0
  36. package/dist/permissions.d.ts.map +1 -0
  37. package/dist/permissions.js +29 -0
  38. package/dist/permissions.js.map +1 -0
  39. package/dist/sql.d.ts +9 -0
  40. package/dist/sql.d.ts.map +1 -0
  41. package/dist/sql.js +15 -0
  42. package/dist/sql.js.map +1 -0
  43. package/dist/store/memory.d.ts +7 -0
  44. package/dist/store/memory.d.ts.map +1 -0
  45. package/dist/store/memory.js +104 -0
  46. package/dist/store/memory.js.map +1 -0
  47. package/dist/store/postgres.d.ts +17 -0
  48. package/dist/store/postgres.d.ts.map +1 -0
  49. package/dist/store/postgres.js +168 -0
  50. package/dist/store/postgres.js.map +1 -0
  51. package/dist/store/types.d.ts +105 -0
  52. package/dist/store/types.d.ts.map +1 -0
  53. package/dist/store/types.js +2 -0
  54. package/dist/store/types.js.map +1 -0
  55. package/package.json +58 -4
  56. package/sql/0001_bexio_grants.sql +32 -0
@@ -0,0 +1,168 @@
1
+ const COLUMNS = `id, owner_id, bexio_company_id, bexio_user_id, bexio_company_user_id,
2
+ company_name, user_name, user_email, role, status, access_token, refresh_token,
3
+ access_expires_at, refresh_expires_at, scope, refresh_locked_until,
4
+ last_refreshed_at, created_at, updated_at`;
5
+ function date(value) {
6
+ if (value === null)
7
+ return null;
8
+ return value instanceof Date ? value : new Date(value);
9
+ }
10
+ function toRecord(row) {
11
+ return {
12
+ id: row.id,
13
+ ownerId: row.owner_id,
14
+ companyId: row.bexio_company_id,
15
+ userId: row.bexio_user_id,
16
+ companyUserId: row.bexio_company_user_id === null
17
+ ? null
18
+ : Number(row.bexio_company_user_id),
19
+ companyName: row.company_name,
20
+ userName: row.user_name,
21
+ userEmail: row.user_email,
22
+ role: row.role,
23
+ status: row.status,
24
+ accessToken: row.access_token,
25
+ refreshToken: row.refresh_token,
26
+ accessExpiresAt: date(row.access_expires_at),
27
+ refreshExpiresAt: date(row.refresh_expires_at),
28
+ scope: row.scope,
29
+ refreshLockedUntil: date(row.refresh_locked_until),
30
+ lastRefreshedAt: date(row.last_refreshed_at),
31
+ createdAt: date(row.created_at),
32
+ updatedAt: date(row.updated_at),
33
+ };
34
+ }
35
+ /**
36
+ * Grant store on the `bexio_grants` table (create it with
37
+ * `npx @soulcode/bexio-oauth sql`). Every operation is a single statement,
38
+ * so it works through transaction-mode poolers and HTTP drivers.
39
+ */
40
+ export function postgresStore(sql) {
41
+ const one = async (text, params) => {
42
+ const { rows } = await sql.query(text, params);
43
+ return rows[0] ? toRecord(rows[0]) : null;
44
+ };
45
+ const many = async (text, params) => {
46
+ const { rows } = await sql.query(text, params);
47
+ return rows.map(toRecord);
48
+ };
49
+ return {
50
+ async upsertGrant(g) {
51
+ const row = await one(`INSERT INTO bexio_grants (owner_id, bexio_company_id, bexio_user_id,
52
+ bexio_company_user_id, company_name, user_name, user_email, role, status,
53
+ access_token, refresh_token, access_expires_at, refresh_expires_at, scope,
54
+ refresh_locked_until, last_refreshed_at, created_at, updated_at)
55
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'active', $9, $10, $11, $12, $13, NULL, $14, $14, $14)
56
+ ON CONFLICT (owner_id, bexio_company_id, bexio_user_id) DO UPDATE SET
57
+ bexio_company_user_id = EXCLUDED.bexio_company_user_id,
58
+ company_name = EXCLUDED.company_name,
59
+ user_name = EXCLUDED.user_name,
60
+ user_email = EXCLUDED.user_email,
61
+ role = EXCLUDED.role,
62
+ status = 'active',
63
+ access_token = EXCLUDED.access_token,
64
+ refresh_token = EXCLUDED.refresh_token,
65
+ access_expires_at = EXCLUDED.access_expires_at,
66
+ refresh_expires_at = EXCLUDED.refresh_expires_at,
67
+ scope = EXCLUDED.scope,
68
+ refresh_locked_until = NULL,
69
+ refresh_lock_token = NULL,
70
+ last_refreshed_at = EXCLUDED.last_refreshed_at,
71
+ updated_at = EXCLUDED.updated_at
72
+ RETURNING ${COLUMNS}`, [
73
+ g.ownerId,
74
+ g.companyId,
75
+ g.userId,
76
+ g.companyUserId,
77
+ g.companyName,
78
+ g.userName,
79
+ g.userEmail,
80
+ g.role,
81
+ g.accessToken,
82
+ g.refreshToken,
83
+ g.accessExpiresAt,
84
+ g.refreshExpiresAt,
85
+ g.scope,
86
+ g.now,
87
+ ]);
88
+ if (!row)
89
+ throw new Error("upsert returned no row");
90
+ return row;
91
+ },
92
+ async findGrants({ ownerId, companyId, grantId }) {
93
+ return many(`SELECT ${COLUMNS} FROM bexio_grants
94
+ WHERE owner_id = $1
95
+ AND ($2::text IS NULL OR bexio_company_id = $2)
96
+ AND ($3::uuid IS NULL OR id = $3::uuid)
97
+ ORDER BY created_at, id`, [ownerId, companyId ?? null, grantId ?? null]);
98
+ },
99
+ async getGrant(id) {
100
+ return one(`SELECT ${COLUMNS} FROM bexio_grants WHERE id = $1`, [id]);
101
+ },
102
+ async acquireRefreshLock({ id, now, lockUntil, lockToken, accessExpiresBefore, refreshedBefore, }) {
103
+ return one(`UPDATE bexio_grants SET refresh_locked_until = $3, refresh_lock_token = $6
104
+ WHERE id = $1
105
+ AND status = 'active'
106
+ AND (refresh_locked_until IS NULL OR refresh_locked_until <= $2)
107
+ AND (
108
+ ($4::timestamptz IS NOT NULL AND (access_expires_at IS NULL OR access_expires_at <= $4))
109
+ OR ($5::timestamptz IS NOT NULL AND last_refreshed_at < $5)
110
+ )
111
+ RETURNING ${COLUMNS}`, [
112
+ id,
113
+ now,
114
+ lockUntil,
115
+ accessExpiresBefore ?? null,
116
+ refreshedBefore ?? null,
117
+ lockToken,
118
+ ]);
119
+ },
120
+ // Holder writes are fenced by refresh_lock_token: a holder whose lock
121
+ // expired or was cleared by a reconnect matches no row and changes nothing.
122
+ async saveRefreshed(id, lockToken, r) {
123
+ return one(`UPDATE bexio_grants SET
124
+ access_token = $2,
125
+ refresh_token = COALESCE($3, refresh_token),
126
+ access_expires_at = $4,
127
+ refresh_expires_at = COALESCE($5, refresh_expires_at),
128
+ scope = COALESCE($6, scope),
129
+ role = COALESCE($7, role),
130
+ status = 'active',
131
+ refresh_locked_until = NULL,
132
+ refresh_lock_token = NULL,
133
+ last_refreshed_at = $8,
134
+ updated_at = $8
135
+ WHERE id = $1 AND refresh_lock_token = $9
136
+ RETURNING ${COLUMNS}`, [
137
+ id,
138
+ r.accessToken,
139
+ r.refreshToken,
140
+ r.accessExpiresAt,
141
+ r.refreshExpiresAt,
142
+ r.scope,
143
+ r.role,
144
+ r.now,
145
+ lockToken,
146
+ ]);
147
+ },
148
+ async markReauthRequired(id, lockToken, now) {
149
+ await sql.query(`UPDATE bexio_grants SET status = 'reauth_required',
150
+ refresh_locked_until = NULL, refresh_lock_token = NULL, updated_at = $2
151
+ WHERE id = $1 AND refresh_lock_token = $3`, [id, now, lockToken]);
152
+ },
153
+ async releaseRefreshLock(id, lockToken) {
154
+ await sql.query(`UPDATE bexio_grants SET refresh_locked_until = NULL, refresh_lock_token = NULL
155
+ WHERE id = $1 AND refresh_lock_token = $2`, [id, lockToken]);
156
+ },
157
+ async deleteGrant(id) {
158
+ await sql.query(`DELETE FROM bexio_grants WHERE id = $1`, [id]);
159
+ },
160
+ async listIdleGrants({ refreshedBefore, limit }) {
161
+ return many(`SELECT ${COLUMNS} FROM bexio_grants
162
+ WHERE status = 'active' AND last_refreshed_at < $1
163
+ ORDER BY last_refreshed_at
164
+ LIMIT $2`, [refreshedBefore, limit]);
165
+ },
166
+ };
167
+ }
168
+ //# 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":""}
package/package.json CHANGED
@@ -1,6 +1,60 @@
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.1.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
+ "build": "tsc -p tsconfig.build.json",
42
+ "typecheck": "tsc -p tsconfig.json --noEmit",
43
+ "test": "vitest run",
44
+ "test:watch": "vitest",
45
+ "prepack": "npm run build"
46
+ },
47
+ "dependencies": {
48
+ "jose": "^6.2.13",
49
+ "openid-client": "^6.8.8"
50
+ },
51
+ "devDependencies": {
52
+ "@types/pg": "8.23.1",
53
+ "mock-bexio": "*",
54
+ "pg": "8.23.1",
55
+ "vitest": "5.0.5"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
59
+ }
60
+ }
@@ -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';