@ttoss/auth-postgresdb 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.
package/dist/index.mjs ADDED
@@ -0,0 +1,475 @@
1
+ /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
+ import { hashAuthorizationCode } from "@ttoss/auth-core";
3
+ import { Column, DataType, Model, PrimaryKey, Table } from "@ttoss/postgresdb";
4
+
5
+ //#region src/stores/createAuthCodeStore.ts
6
+ /**
7
+ * Creates an {@link AuthCodeStore} backed by the `oauth_auth_codes` table,
8
+ * storing codes by SHA-256 hash.
9
+ *
10
+ * The engine hands the store the plaintext code on every call and never compares
11
+ * the returned `code` against anything — it reads only `clientId`,
12
+ * `redirectUri`, `codeChallenge`, `scopes`, `subject`, and `expiresAt`. So the
13
+ * adapter hashes to find the row and echoes the presented value back, and a
14
+ * database dump yields no replayable codes.
15
+ */
16
+ var createAuthCodeStore = ({
17
+ model
18
+ }) => {
19
+ return {
20
+ save: async code => {
21
+ await model.upsert({
22
+ codeHash: hashAuthorizationCode({
23
+ code: code.code
24
+ }),
25
+ clientId: code.clientId,
26
+ redirectUri: code.redirectUri,
27
+ codeChallenge: code.codeChallenge,
28
+ scopes: code.scopes,
29
+ subject: code.subject,
30
+ expiresAt: new Date(code.expiresAt)
31
+ });
32
+ },
33
+ get: async code => {
34
+ const row = await model.findByPk(hashAuthorizationCode({
35
+ code
36
+ }));
37
+ if (!row) return;
38
+ return {
39
+ code,
40
+ clientId: row.clientId,
41
+ redirectUri: row.redirectUri,
42
+ codeChallenge: row.codeChallenge,
43
+ scopes: row.scopes,
44
+ subject: row.subject,
45
+ expiresAt: row.expiresAt.getTime()
46
+ };
47
+ },
48
+ delete: async code => {
49
+ await model.destroy({
50
+ where: {
51
+ codeHash: hashAuthorizationCode({
52
+ code
53
+ })
54
+ }
55
+ });
56
+ }
57
+ };
58
+ };
59
+
60
+ //#endregion
61
+ //#region src/stores/createClientStore.ts
62
+ /**
63
+ * Registration fields that have their own column. Everything else a client
64
+ * submits goes to `metadata`, so the document round-trips unchanged.
65
+ */
66
+ var COLUMN_FIELDS = ["client_id", "client_secret", "client_name", "redirect_uris", "grant_types", "response_types", "token_endpoint_auth_method", "scope", "client_id_issued_at"];
67
+ var toRecord = row => {
68
+ return {
69
+ ...row.metadata,
70
+ client_id: row.clientId,
71
+ redirect_uris: row.redirectUris,
72
+ ...(row.clientSecret !== null && {
73
+ client_secret: row.clientSecret
74
+ }),
75
+ ...(row.clientName !== null && {
76
+ client_name: row.clientName
77
+ }),
78
+ ...(row.grantTypes !== null && {
79
+ grant_types: row.grantTypes
80
+ }),
81
+ ...(row.responseTypes !== null && {
82
+ response_types: row.responseTypes
83
+ }),
84
+ ...(row.tokenEndpointAuthMethod !== null && {
85
+ token_endpoint_auth_method: row.tokenEndpointAuthMethod
86
+ }),
87
+ ...(row.scope !== null && {
88
+ scope: row.scope
89
+ }),
90
+ ...(row.clientIdIssuedAt !== null && {
91
+ client_id_issued_at: Number(row.clientIdIssuedAt)
92
+ })
93
+ };
94
+ };
95
+ var toMetadata = client => {
96
+ const metadata = {};
97
+ for (const [key, value] of Object.entries(client)) if (!COLUMN_FIELDS.includes(key)) metadata[key] = value;
98
+ return metadata;
99
+ };
100
+ /**
101
+ * Creates a {@link ClientStore} backed by the `oauth_clients` table.
102
+ *
103
+ * `register` upserts, so re-registering an existing `client_id` replaces the
104
+ * stored document rather than failing on the primary key.
105
+ */
106
+ var createClientStore = ({
107
+ model
108
+ }) => {
109
+ return {
110
+ get: async clientId => {
111
+ const row = await model.findByPk(clientId);
112
+ return row ? toRecord(row) : void 0;
113
+ },
114
+ register: async client => {
115
+ await model.upsert({
116
+ clientId: client.client_id,
117
+ clientSecret: client.client_secret ?? null,
118
+ clientName: client.client_name ?? null,
119
+ redirectUris: client.redirect_uris,
120
+ grantTypes: client.grant_types ?? null,
121
+ responseTypes: client.response_types ?? null,
122
+ tokenEndpointAuthMethod: client.token_endpoint_auth_method ?? null,
123
+ scope: client.scope ?? null,
124
+ clientIdIssuedAt: client.client_id_issued_at ?? null,
125
+ metadata: toMetadata(client)
126
+ });
127
+ }
128
+ };
129
+ };
130
+
131
+ //#endregion
132
+ //#region src/stores/createConsentStore.ts
133
+ /**
134
+ * Creates a consent store backed by the `oauth_consents` table, for use with
135
+ * `createRedirectConsentOnAuthorize`.
136
+ *
137
+ * Reads and deletes satisfy `ConsentGrantStore`; `saveConsentGrant` covers the
138
+ * write the consent page performs. Grants are single-use — the `onAuthorize`
139
+ * hook deletes each one as it consumes it.
140
+ */
141
+ var createConsentStore = ({
142
+ model
143
+ }) => {
144
+ return {
145
+ saveConsentGrant: async ({
146
+ codeChallenge,
147
+ subject,
148
+ scopes,
149
+ expiresAt
150
+ }) => {
151
+ await model.upsert({
152
+ codeChallenge,
153
+ subject,
154
+ scopes,
155
+ expiresAt: new Date(expiresAt)
156
+ });
157
+ },
158
+ getConsentGrant: async ({
159
+ codeChallenge
160
+ }) => {
161
+ const row = await model.findByPk(codeChallenge);
162
+ if (!row) return;
163
+ return {
164
+ subject: row.subject,
165
+ scopes: row.scopes,
166
+ expiresAt: row.expiresAt.getTime()
167
+ };
168
+ },
169
+ deleteConsentGrant: async ({
170
+ codeChallenge
171
+ }) => {
172
+ await model.destroy({
173
+ where: {
174
+ codeChallenge
175
+ }
176
+ });
177
+ }
178
+ };
179
+ };
180
+
181
+ //#endregion
182
+ //#region src/stores/createRefreshTokenStore.ts
183
+ /**
184
+ * Creates a {@link RefreshTokenStore} backed by the `oauth_refresh_tokens`
185
+ * table, keyed by token hash and by the `(clientId, subject)` owner that
186
+ * `deleteByOwner` revokes on reuse detection.
187
+ */
188
+ var createRefreshTokenStore = ({
189
+ model
190
+ }) => {
191
+ return {
192
+ save: async token => {
193
+ await model.upsert({
194
+ tokenHash: token.tokenHash,
195
+ clientId: token.clientId,
196
+ subject: token.subject,
197
+ scopes: token.scopes,
198
+ expiresAt: new Date(token.expiresAt),
199
+ consumedAt: token.consumedAt === void 0 ? null : new Date(token.consumedAt)
200
+ });
201
+ },
202
+ get: async tokenHash => {
203
+ const row = await model.findByPk(tokenHash);
204
+ if (!row) return;
205
+ return {
206
+ tokenHash: row.tokenHash,
207
+ clientId: row.clientId,
208
+ subject: row.subject,
209
+ scopes: row.scopes,
210
+ expiresAt: row.expiresAt.getTime(),
211
+ /**
212
+ * The key is omitted, not set to `null`. Rotation tests
213
+ * `stored.consumedAt !== undefined` to detect reuse, and a nullable
214
+ * timestamp column reads back as `null` — which would make every live
215
+ * token look consumed, so the first refresh would be treated as a
216
+ * replay and revoke the owner's entire token set.
217
+ */
218
+ ...(row.consumedAt !== null && {
219
+ consumedAt: row.consumedAt.getTime()
220
+ })
221
+ };
222
+ },
223
+ delete: async tokenHash => {
224
+ await model.destroy({
225
+ where: {
226
+ tokenHash
227
+ }
228
+ });
229
+ },
230
+ deleteByOwner: async ({
231
+ clientId,
232
+ subject
233
+ }) => {
234
+ await model.destroy({
235
+ where: {
236
+ clientId,
237
+ subject
238
+ }
239
+ });
240
+ }
241
+ };
242
+ };
243
+
244
+ //#endregion
245
+ //#region src/createPostgresdbOAuthStores.ts
246
+ /**
247
+ * Creates every durable OAuth store from a `@ttoss/postgresdb` `db` handle.
248
+ *
249
+ * The stores are mechanical adapters between the `@ttoss/auth-core` store
250
+ * contracts and Sequelize, so an app that already uses `@ttoss/postgresdb` does
251
+ * not have to write them — nor reach around its own ORM to inject a raw `pg`
252
+ * query runner.
253
+ *
254
+ * @example
255
+ * ```typescript
256
+ * import { createPostgresdbOAuthStores, oauthModels } from '@ttoss/auth-postgresdb';
257
+ * import { initialize } from '@ttoss/postgresdb';
258
+ *
259
+ * const db = await initialize({ models: { ...oauthModels, User } });
260
+ *
261
+ * const { clientStore, authCodeStore, consentStore, refreshTokenStore } =
262
+ * createPostgresdbOAuthStores({ db });
263
+ * ```
264
+ */
265
+ var createPostgresdbOAuthStores = ({
266
+ db
267
+ }) => {
268
+ return {
269
+ clientStore: createClientStore({
270
+ model: db.OAuthClient
271
+ }),
272
+ authCodeStore: createAuthCodeStore({
273
+ model: db.OAuthAuthCode
274
+ }),
275
+ consentStore: createConsentStore({
276
+ model: db.OAuthConsent
277
+ }),
278
+ refreshTokenStore: createRefreshTokenStore({
279
+ model: db.OAuthRefreshToken
280
+ })
281
+ };
282
+ };
283
+
284
+ //#endregion
285
+ //#region \0@oxc-project+runtime@0.134.0/helpers/esm/decorateMetadata.js
286
+ function __decorateMetadata(k, v) {
287
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
288
+ }
289
+
290
+ //#endregion
291
+ //#region \0@oxc-project+runtime@0.134.0/helpers/esm/decorate.js
292
+ function __decorate(decorators, target, key, desc) {
293
+ var c = arguments.length,
294
+ r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc,
295
+ d;
296
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
297
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
298
+ }
299
+
300
+ //#endregion
301
+ //#region src/models/OAuthAuthCode.ts
302
+ var OAuthAuthCode = class OAuthAuthCode extends Model {};
303
+ __decorate([PrimaryKey, Column({
304
+ type: DataType.STRING,
305
+ allowNull: false
306
+ }), __decorateMetadata("design:type", String)], OAuthAuthCode.prototype, "codeHash", void 0);
307
+ __decorate([Column({
308
+ type: DataType.STRING,
309
+ allowNull: false
310
+ }), __decorateMetadata("design:type", String)], OAuthAuthCode.prototype, "clientId", void 0);
311
+ __decorate([Column({
312
+ type: DataType.TEXT,
313
+ allowNull: false
314
+ }), __decorateMetadata("design:type", String)], OAuthAuthCode.prototype, "redirectUri", void 0);
315
+ __decorate([Column({
316
+ type: DataType.STRING,
317
+ allowNull: false
318
+ }), __decorateMetadata("design:type", String)], OAuthAuthCode.prototype, "codeChallenge", void 0);
319
+ __decorate([Column({
320
+ type: DataType.JSONB,
321
+ allowNull: false
322
+ }), __decorateMetadata("design:type", Array)], OAuthAuthCode.prototype, "scopes", void 0);
323
+ __decorate([Column({
324
+ type: DataType.STRING,
325
+ allowNull: false
326
+ }), __decorateMetadata("design:type", String)], OAuthAuthCode.prototype, "subject", void 0);
327
+ __decorate([Column({
328
+ type: DataType.DATE,
329
+ allowNull: false
330
+ }), __decorateMetadata("design:type", typeof Date === "undefined" ? Object : Date)], OAuthAuthCode.prototype, "expiresAt", void 0);
331
+ OAuthAuthCode = __decorate([Table({
332
+ tableName: "oauth_auth_codes",
333
+ modelName: "oauthAuthCode",
334
+ indexes: [{
335
+ fields: ["expires_at"]
336
+ }]
337
+ })], OAuthAuthCode);
338
+
339
+ //#endregion
340
+ //#region src/models/OAuthClient.ts
341
+ var OAuthClient = class OAuthClient extends Model {};
342
+ __decorate([PrimaryKey, Column({
343
+ type: DataType.STRING,
344
+ allowNull: false
345
+ }), __decorateMetadata("design:type", String)], OAuthClient.prototype, "clientId", void 0);
346
+ __decorate([Column({
347
+ type: DataType.STRING,
348
+ allowNull: true
349
+ }), __decorateMetadata("design:type", Object)], OAuthClient.prototype, "clientSecret", void 0);
350
+ __decorate([Column({
351
+ type: DataType.STRING,
352
+ allowNull: true
353
+ }), __decorateMetadata("design:type", Object)], OAuthClient.prototype, "clientName", void 0);
354
+ __decorate([Column({
355
+ type: DataType.JSONB,
356
+ allowNull: false
357
+ }), __decorateMetadata("design:type", Array)], OAuthClient.prototype, "redirectUris", void 0);
358
+ __decorate([Column({
359
+ type: DataType.JSONB,
360
+ allowNull: true
361
+ }), __decorateMetadata("design:type", Object)], OAuthClient.prototype, "grantTypes", void 0);
362
+ __decorate([Column({
363
+ type: DataType.JSONB,
364
+ allowNull: true
365
+ }), __decorateMetadata("design:type", Object)], OAuthClient.prototype, "responseTypes", void 0);
366
+ __decorate([Column({
367
+ type: DataType.STRING,
368
+ allowNull: true
369
+ }), __decorateMetadata("design:type", Object)], OAuthClient.prototype, "tokenEndpointAuthMethod", void 0);
370
+ __decorate([Column({
371
+ type: DataType.STRING,
372
+ allowNull: true
373
+ }), __decorateMetadata("design:type", Object)], OAuthClient.prototype, "scope", void 0);
374
+ __decorate([Column({
375
+ type: DataType.BIGINT,
376
+ allowNull: true
377
+ }), __decorateMetadata("design:type", Object)], OAuthClient.prototype, "clientIdIssuedAt", void 0);
378
+ __decorate([Column({
379
+ type: DataType.JSONB,
380
+ allowNull: false,
381
+ defaultValue: {}
382
+ }), __decorateMetadata("design:type", typeof Record === "undefined" ? Object : Record)], OAuthClient.prototype, "metadata", void 0);
383
+ OAuthClient = __decorate([Table({
384
+ tableName: "oauth_clients",
385
+ modelName: "oauthClient"
386
+ })], OAuthClient);
387
+
388
+ //#endregion
389
+ //#region src/models/OAuthConsent.ts
390
+ var OAuthConsent = class OAuthConsent extends Model {};
391
+ __decorate([PrimaryKey, Column({
392
+ type: DataType.STRING,
393
+ allowNull: false
394
+ }), __decorateMetadata("design:type", String)], OAuthConsent.prototype, "codeChallenge", void 0);
395
+ __decorate([Column({
396
+ type: DataType.STRING,
397
+ allowNull: false
398
+ }), __decorateMetadata("design:type", String)], OAuthConsent.prototype, "subject", void 0);
399
+ __decorate([Column({
400
+ type: DataType.JSONB,
401
+ allowNull: false
402
+ }), __decorateMetadata("design:type", Array)], OAuthConsent.prototype, "scopes", void 0);
403
+ __decorate([Column({
404
+ type: DataType.DATE,
405
+ allowNull: false
406
+ }), __decorateMetadata("design:type", typeof Date === "undefined" ? Object : Date)], OAuthConsent.prototype, "expiresAt", void 0);
407
+ OAuthConsent = __decorate([Table({
408
+ tableName: "oauth_consents",
409
+ modelName: "oauthConsent",
410
+ indexes: [{
411
+ fields: ["expires_at"]
412
+ }]
413
+ })], OAuthConsent);
414
+
415
+ //#endregion
416
+ //#region src/models/OAuthRefreshToken.ts
417
+ var OAuthRefreshToken = class OAuthRefreshToken extends Model {};
418
+ __decorate([PrimaryKey, Column({
419
+ type: DataType.STRING,
420
+ allowNull: false
421
+ }), __decorateMetadata("design:type", String)], OAuthRefreshToken.prototype, "tokenHash", void 0);
422
+ __decorate([Column({
423
+ type: DataType.STRING,
424
+ allowNull: false
425
+ }), __decorateMetadata("design:type", String)], OAuthRefreshToken.prototype, "clientId", void 0);
426
+ __decorate([Column({
427
+ type: DataType.STRING,
428
+ allowNull: false
429
+ }), __decorateMetadata("design:type", String)], OAuthRefreshToken.prototype, "subject", void 0);
430
+ __decorate([Column({
431
+ type: DataType.JSONB,
432
+ allowNull: false
433
+ }), __decorateMetadata("design:type", Array)], OAuthRefreshToken.prototype, "scopes", void 0);
434
+ __decorate([Column({
435
+ type: DataType.DATE,
436
+ allowNull: false
437
+ }), __decorateMetadata("design:type", typeof Date === "undefined" ? Object : Date)], OAuthRefreshToken.prototype, "expiresAt", void 0);
438
+ __decorate([Column({
439
+ type: DataType.DATE,
440
+ allowNull: true
441
+ }), __decorateMetadata("design:type", Object)], OAuthRefreshToken.prototype, "consumedAt", void 0);
442
+ OAuthRefreshToken = __decorate([Table({
443
+ tableName: "oauth_refresh_tokens",
444
+ modelName: "oauthRefreshToken",
445
+ indexes: [{
446
+ fields: ["client_id", "subject"]
447
+ }, {
448
+ fields: ["expires_at"]
449
+ }]
450
+ })], OAuthRefreshToken);
451
+
452
+ //#endregion
453
+ //#region src/models/index.ts
454
+ /**
455
+ * The OAuth data model, ready to register alongside the application's own
456
+ * models so `ttoss-postgresdb sync` and `erd` cover these tables like any
457
+ * other instead of them being a side-channel schema the CLI cannot see.
458
+ *
459
+ * @example
460
+ * ```typescript
461
+ * import { oauthModels } from '@ttoss/auth-postgresdb';
462
+ * import { initialize } from '@ttoss/postgresdb';
463
+ *
464
+ * export const db = await initialize({ models: { ...oauthModels, User } });
465
+ * ```
466
+ */
467
+ var oauthModels = {
468
+ OAuthAuthCode,
469
+ OAuthClient,
470
+ OAuthConsent,
471
+ OAuthRefreshToken
472
+ };
473
+
474
+ //#endregion
475
+ export { OAuthAuthCode, OAuthClient, OAuthConsent, OAuthRefreshToken, createAuthCodeStore, createClientStore, createConsentStore, createPostgresdbOAuthStores, createRefreshTokenStore, oauthModels };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@ttoss/auth-postgresdb",
3
+ "version": "0.1.0",
4
+ "description": "Durable Postgres OAuth stores and models for @ttoss/auth-core",
5
+ "keywords": [
6
+ "auth",
7
+ "oauth",
8
+ "postgres",
9
+ "sequelize",
10
+ "ttoss"
11
+ ],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/ttoss/ttoss.git",
15
+ "directory": "packages/auth-postgresdb"
16
+ },
17
+ "license": "MIT",
18
+ "author": "ttoss",
19
+ "contributors": [
20
+ "Pedro Arantes <pedro@arantespp.com> (https://arantespp.com)"
21
+ ],
22
+ "sideEffects": false,
23
+ "type": "module",
24
+ "exports": {
25
+ ".": {
26
+ "import": "./dist/index.mjs",
27
+ "require": "./dist/index.cjs",
28
+ "types": "./dist/index.d.mts"
29
+ }
30
+ },
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "devDependencies": {
35
+ "jest": "^30.4.2",
36
+ "tsdown": "^0.22.2",
37
+ "@ttoss/config": "^1.37.17",
38
+ "@ttoss/postgresdb": "^0.11.0",
39
+ "@ttoss/auth-core": "^0.13.0"
40
+ },
41
+ "peerDependencies": {
42
+ "@ttoss/auth-core": "^0.13.0",
43
+ "@ttoss/postgresdb": "^0.11.0"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public",
47
+ "provenance": true
48
+ },
49
+ "scripts": {
50
+ "build": "tsdown",
51
+ "pretest": "pnpm run build",
52
+ "test": "jest --projects tests/unit"
53
+ }
54
+ }