@stacksjs/auth 0.72.102 → 0.73.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/gate.d.ts CHANGED
@@ -43,7 +43,7 @@ export declare function defineGates<const T extends GatesDefinition>(definition:
43
43
  * define('edit-settings', (user) => user?.isAdmin)
44
44
  * define('update-post', (user, post) => user?.id === post.userId)
45
45
  */
46
- export declare function define<T = any>(ability: string, callback: GateCallback<T>): void;
46
+ export declare function define<T = any>(ability: Ability, callback: GateCallback<T>): void;
47
47
  /**
48
48
  * Register a policy for a model
49
49
  *
@@ -73,28 +73,34 @@ export declare function after(callback: GateAfterCallback): void;
73
73
  * if (await allows('edit-settings', user)) { ... }
74
74
  * if (await allows('update', user, post)) { ... }
75
75
  */
76
- export declare function allows(ability: Ability, user: UserModel | null, ...args: any[]): Promise<boolean>;
76
+ export declare function allows<const A extends Ability>(ability: A, user: UserModel | null, ...args: AbilityArgs<A>): Promise<boolean>;
77
77
  /**
78
78
  * Check if the user is denied from performing an ability
79
79
  *
80
80
  * @example
81
81
  * if (await denies('delete', user, post)) { ... }
82
82
  */
83
- export declare function denies(ability: Ability, user: UserModel | null, ...args: any[]): Promise<boolean>;
83
+ export declare function denies<const A extends Ability>(ability: A, user: UserModel | null, ...args: AbilityArgs<A>): Promise<boolean>;
84
84
  /**
85
85
  * Check if the user can perform an ability (alias for allows)
86
86
  */
87
- export declare function can(ability: Ability, user: UserModel | null, ...args: any[]): Promise<boolean>;
87
+ export declare function can<const A extends Ability>(ability: A, user: UserModel | null, ...args: AbilityArgs<A>): Promise<boolean>;
88
88
  /**
89
89
  * Check if the user cannot perform an ability (alias for denies)
90
90
  */
91
- export declare function cannot(ability: Ability, user: UserModel | null, ...args: any[]): Promise<boolean>;
91
+ export declare function cannot<const A extends Ability>(ability: A, user: UserModel | null, ...args: AbilityArgs<A>): Promise<boolean>;
92
92
  /**
93
93
  * Check if the user can perform any of the given abilities
94
94
  *
95
95
  * @example
96
96
  * if (await any(['update', 'delete'], user, post)) { ... }
97
97
  */
98
+ /*
99
+ * `any` / `all` / `none` keep `any[]`: one argument list is checked against
100
+ * SEVERAL abilities, which may each declare different parameters, so there is
101
+ * no single tuple that is correct for the call. The single-ability functions
102
+ * above are where the declaration can be enforced.
103
+ */
98
104
  export declare function any(abilities: readonly Ability[], user: UserModel | null, ...args: any[]): Promise<boolean>;
99
105
  /**
100
106
  * Check if the user can perform all of the given abilities
@@ -113,11 +119,11 @@ export declare function none(abilities: readonly Ability[], user: UserModel | nu
113
119
  * @example
114
120
  * await authorize('update', user, post) // Throws if not allowed
115
121
  */
116
- export declare function authorize(ability: Ability, user: UserModel | null, ...args: any[]): Promise<AuthorizationResponse>;
122
+ export declare function authorize<const A extends Ability>(ability: A, user: UserModel | null, ...args: AbilityArgs<A>): Promise<AuthorizationResponse>;
117
123
  /**
118
124
  * Get detailed inspection result for an ability check
119
125
  */
120
- export declare function inspect(ability: Ability, user: UserModel | null, ...args: any[]): Promise<AuthorizationResponse>;
126
+ export declare function inspect<const A extends Ability>(ability: A, user: UserModel | null, ...args: AbilityArgs<A>): Promise<AuthorizationResponse>;
121
127
  /**
122
128
  * Get a policy instance for a model
123
129
  */
@@ -246,6 +252,17 @@ export type PolicyAbility = 'viewAny' | 'view' | 'create' | 'update' | 'delete'
246
252
  */
247
253
  // eslint-disable-next-line ts/ban-types -- `string & {}` keeps literal completions alive
248
254
  export type Ability = GateName | PolicyAbility | (string & {});
255
+ /**
256
+ * The arguments an ability takes after the user.
257
+ *
258
+ * A gate the application declares contributes its own parameter list, so
259
+ * `Gate.allows('update-post', user, post)` is checked against how the gate was
260
+ * written. Anything else - a policy ability, or a name computed at runtime -
261
+ * keeps `any[]`, which is what it was for every ability before.
262
+ */
263
+ export type AbilityArgs<A extends Ability> = A extends keyof AppGates
264
+ ? (AppGates[A] extends readonly unknown[] ? AppGates[A] : any[])
265
+ : any[];
249
266
  /** A policy class name, as narrow as the application has made it. */
250
267
  export type PolicyName = keyof PolicyClasses extends never ? string : keyof PolicyClasses & string;
251
268
  /** A model name a policy may be registered for. */
@@ -1,3 +1,3 @@
1
- import{createHash,randomBytes}from"node:crypto";import process from"node:process";import{config}from"@stacksjs/config";import{db,sqlDateTime}from"@stacksjs/database";import{mail,template}from"@stacksjs/email";import{log}from"@stacksjs/logging";import{RateLimiter}from"./rate-limiter";function hashToken(raw){return createHash("sha256").update(raw).digest("hex")}function expireMinutes(){return config.auth.magicLink?.expire??15}function safeRedirect(candidate){const fallback=config.auth.magicLink?.redirectDefault??"/";if(!candidate)return fallback;if(!candidate.startsWith("/")||candidate.startsWith("//"))return fallback;return candidate}async function linkBase(siteId){if(siteId){const primary=await db.selectFrom("site_domains").where("site_id","=",siteId).where("is_primary","=",!0).where("verified_at","is not",null).select(["domain"]).executeTakeFirst();if(primary?.domain)return`https://${primary.domain}`;const site=await db.selectFrom("sites").where("id","=",siteId).select(["subdomain"]).executeTakeFirst(),base=config.sites?.baseDomain;if(site?.subdomain&&base)return`https://${site.subdomain}.${base}`}return config.app.url?`https://${config.app.url}`:`http://localhost:${process.env.PORT||"3000"}`}export async function sendMagicLink(email,options={}){const normalized=email.trim().toLowerCase();if(await RateLimiter.isRateLimited(normalized))return;await RateLimiter.recordFailedAttempt(normalized);let user=await db.selectFrom("users").where("email","=",normalized).select(["id","email"]).executeTakeFirst();if(!user&&options.createUser){await db.insertInto("users").values({email:normalized,name:normalized.split("@")[0],password:null,created_at:sqlDateTime(new Date),updated_at:sqlDateTime(new Date)}).execute();user=await db.selectFrom("users").where("email","=",normalized).select(["id","email"]).executeTakeFirst()}if(!user)return;const raw=randomBytes(32).toString("base64url"),ttl=options.ttlMinutes??expireMinutes(),redirectTo=safeRedirect(options.redirectTo);await db.deleteFrom("magic_link_tokens").where("email","=",normalized).where("consumed_at","is",null).execute();await db.insertInto("magic_link_tokens").values({email:normalized,user_id:user.id,token:hashToken(raw),expires_at:sqlDateTime(new Date(Date.now()+ttl*60000)),redirect_to:redirectTo,site_id:options.siteId??null,created_at:sqlDateTime(new Date),updated_at:sqlDateTime(new Date)}).execute();const base=await linkBase(options.siteId),filled=(config.auth.magicLink?.url??"/auth/magic/{token}").replace("{token}",raw),linkUrl=/^https?:\/\//.test(filled)?filled:`${base}${filled.startsWith("/")?"":"/"}${filled}`,appName=config.app.name||"Stacks";let html,text;try{const rendered=await template("magic-link",{subject:`Sign in to ${appName}`,variables:{linkUrl,expireMinutes:ttl}});if(rendered.html||rendered.text){html=rendered.html;text=rendered.text}}catch{}try{await mail.sendOrFail({to:normalized,subject:`Sign in to ${appName}`,...html?{html}:{},text:text||`Sign in to ${appName}: ${linkUrl}
1
+ import{createHash,randomBytes}from"node:crypto";import process from"node:process";import{config}from"@stacksjs/config";import{db,sqlDateTime}from"@stacksjs/database";import{mail,templateByName}from"@stacksjs/email";import{log}from"@stacksjs/logging";import{RateLimiter}from"./rate-limiter";function hashToken(raw){return createHash("sha256").update(raw).digest("hex")}function expireMinutes(){return config.auth.magicLink?.expire??15}function safeRedirect(candidate){const fallback=config.auth.magicLink?.redirectDefault??"/";if(!candidate)return fallback;if(!candidate.startsWith("/")||candidate.startsWith("//"))return fallback;return candidate}async function linkBase(siteId){if(siteId){const primary=await db.selectFrom("site_domains").where("site_id","=",siteId).where("is_primary","=",!0).where("verified_at","is not",null).select(["domain"]).executeTakeFirst();if(primary?.domain)return`https://${primary.domain}`;const site=await db.selectFrom("sites").where("id","=",siteId).select(["subdomain"]).executeTakeFirst(),base=config.sites?.baseDomain;if(site?.subdomain&&base)return`https://${site.subdomain}.${base}`}return config.app.url?`https://${config.app.url}`:`http://localhost:${process.env.PORT||"3000"}`}export async function sendMagicLink(email,options={}){const normalized=email.trim().toLowerCase();if(await RateLimiter.isRateLimited(normalized))return;await RateLimiter.recordFailedAttempt(normalized);let user=await db.selectFrom("users").where("email","=",normalized).select(["id","email"]).executeTakeFirst();if(!user&&options.createUser){await db.insertInto("users").values({email:normalized,name:normalized.split("@")[0],password:null,created_at:sqlDateTime(new Date),updated_at:sqlDateTime(new Date)}).execute();user=await db.selectFrom("users").where("email","=",normalized).select(["id","email"]).executeTakeFirst()}if(!user)return;const raw=randomBytes(32).toString("base64url"),ttl=options.ttlMinutes??expireMinutes(),redirectTo=safeRedirect(options.redirectTo);await db.deleteFrom("magic_link_tokens").where("email","=",normalized).where("consumed_at","is",null).execute();await db.insertInto("magic_link_tokens").values({email:normalized,user_id:user.id,token:hashToken(raw),expires_at:sqlDateTime(new Date(Date.now()+ttl*60000)),redirect_to:redirectTo,site_id:options.siteId??null,created_at:sqlDateTime(new Date),updated_at:sqlDateTime(new Date)}).execute();const base=await linkBase(options.siteId),filled=(config.auth.magicLink?.url??"/auth/magic/{token}").replace("{token}",raw),linkUrl=/^https?:\/\//.test(filled)?filled:`${base}${filled.startsWith("/")?"":"/"}${filled}`,appName=config.app.name||"Stacks";let html,text;try{const rendered=await templateByName("magic-link",{subject:`Sign in to ${appName}`,variables:{linkUrl,expireMinutes:ttl}});if(rendered.html||rendered.text){html=rendered.html;text=rendered.text}}catch{}try{await mail.sendOrFail({to:normalized,subject:`Sign in to ${appName}`,...html?{html}:{},text:text||`Sign in to ${appName}: ${linkUrl}
2
2
 
3
3
  This link works once and expires in ${ttl} minutes. If you didn't request it, ignore this email.`})}catch(error){log.error(`Magic-link email to ${normalized} failed: ${error.message}`)}}export async function consumeMagicLink(raw){if(!raw||raw.length>255)return{ok:!1,reason:"invalid"};const hashed=hashToken(raw),now=sqlDateTime(new Date),claim=await db.updateTable("magic_link_tokens").set({consumed_at:now,updated_at:now}).where("token","=",hashed).where("consumed_at","is",null).where("expires_at",">",now).execute(),updated=typeof claim==="number"?claim:Number(claim?.numUpdatedRows??claim?.changes??0),row=await db.selectFrom("magic_link_tokens").where("token","=",hashed).select(["user_id","email","expires_at","consumed_at","redirect_to"]).executeTakeFirst();if(!row)return{ok:!1,reason:"invalid"};if(updated<1){if(!row.consumed_at&&new Date(row.expires_at).getTime()<=Date.now())return{ok:!1,reason:"expired"};return{ok:!1,reason:"used"}}if(!row.user_id)return{ok:!1,reason:"no-user"};return{ok:!0,userId:Number(row.user_id),email:row.email,redirectTo:safeRedirect(row.redirect_to)}}export async function pruneMagicLinkTokens(olderThanDays=7){const cutoff=sqlDateTime(new Date(Date.now()-olderThanDays*86400000));await db.deleteFrom("magic_link_tokens").where("expires_at","<",cutoff).execute()}
@@ -1,3 +1,4 @@
1
+ export declare function requestToken(request: TokenBearingRequest | null | undefined): string | null;
1
2
  /**
2
3
  * The access token a request carries, from the Authorization header or the
3
4
  * auth cookie.
@@ -13,4 +14,16 @@
13
14
  *
14
15
  * The header is checked first, so an API client behaves exactly as before.
15
16
  */
16
- export declare function requestToken(request: any): string | null;
17
+ /**
18
+ * What this needs off a request, which is deliberately little.
19
+ *
20
+ * Both members are optional because the function is handed two different
21
+ * shapes: a Stacks request, which answers `bearerToken()`, and a plain
22
+ * `Request`, which only has headers. Written out rather than left as `any` so
23
+ * the optional chaining below is checked against something - as `any` it was
24
+ * indistinguishable from probing for members that do not exist on either.
25
+ */
26
+ export declare interface TokenBearingRequest {
27
+ bearerToken?: () => string | undefined | null
28
+ headers?: Headers
29
+ }
@@ -1 +1 @@
1
- import{authCookieToken}from"./cookie";export function requestToken(request){let token=request?.bearerToken?.();if(!token){const header=request?.headers?.get?.("authorization")||request?.headers?.get?.("Authorization");if(typeof header==="string"&&header.startsWith("Bearer "))token=header.substring(7)}if(!token&&request?.headers)token=authCookieToken(request);return token||null}
1
+ import{authCookieToken}from"./cookie";export function requestToken(request){let token=request?.bearerToken?.();if(!token){const header=request?.headers?.get?.("authorization")||request?.headers?.get?.("Authorization");if(typeof header==="string"&&header.startsWith("Bearer "))token=header.substring(7)}if(!token&&request?.headers)token=authCookieToken({headers:request.headers});return token||null}
package/dist/tokens.d.ts CHANGED
@@ -1,19 +1,4 @@
1
1
  import type { AccessToken, CreateClientOptions, CreateClientResult, DatabaseDriver, OAuthClient, PersonalAccessTokenResult, RefreshTokenResult, TokenScopes } from '@stacksjs/types';
2
- /**
3
- * Read `users.password_changed_at` for a user.
4
- *
5
- * Binds a token's validity to the account's credential state: a token
6
- * issued before the user last changed their password is no longer
7
- * trusted, regardless of its own `revoked`/`expires_at` flags. This is
8
- * the durable, use-time backstop behind the post-reset revocation sweep
9
- * (#1947) — even a freshly minted pair that the sweep never saw is
10
- * rejected on first use.
11
- *
12
- * Returns `null` on ANY error (missing column / missing table) so a
13
- * not-yet-migrated database degrades to legacy-allow rather than locking
14
- * everyone out. Accepts an optional query runner so the refresh exchange
15
- * can read the stamp inside its own transaction.
16
- */
17
2
  export declare function getPasswordChangedAt(userId: unknown, q?: { unsafe: (sql: string, params?: any[]) => any }): Promise<Date | null>;
18
3
  /**
19
4
  * True when a credential issued at `createdAt` predates the user's last
package/dist/tokens.js CHANGED
@@ -23,7 +23,7 @@ import{createHash,randomBytes}from"node:crypto";import{db}from"@stacksjs/databas
23
23
  VALUES (?, ?, ?, ?, ?, 0, ?, ?, ?, ${appNow()}, ${appNow()})
24
24
  `,[userId,client.id,hashedToken,name,JSON.stringify(scopes),sqlDateTime(expiresAt),agent,address]);const row=(await db.unsafe(`
25
25
  SELECT * FROM oauth_access_tokens WHERE token = ${param(1)} LIMIT 1
26
- `,[hashedToken]))[0];if(!row)throw new HttpError(500,"Failed to create access token - inserted row not found");const accessToken={id:row.id,userId:row.user_id,clientId:row.oauth_client_id,name:row.name,scopes:parseScopes(row.scopes),revoked:!1,expiresAt,createdAt:new Date(row.created_at),updatedAt:new Date(row.updated_at)};let refreshTokenPlain;if(withRefreshToken){refreshTokenPlain=generateSecureToken(40);const hashedRefreshToken=hashToken(refreshTokenPlain),refreshExpiresAt=new Date;refreshExpiresAt.setDate(refreshExpiresAt.getDate()+refreshExpiresInDays);if(isPostgres)await db.unsafe(`
26
+ `,[hashedToken]))[0];if(!row)throw new HttpError(500,"Failed to create access token - inserted row not found");const accessToken={id:row.id,userId:row.user_id,clientId:row.oauth_client_id,name:row.name,scopes:parseScopes(row.scopes),revoked:!1,expiresAt,createdAt:new Date(row.created_at),updatedAt:row.updated_at?new Date(row.updated_at):new Date(row.created_at)};let refreshTokenPlain;if(withRefreshToken){refreshTokenPlain=generateSecureToken(40);const hashedRefreshToken=hashToken(refreshTokenPlain),refreshExpiresAt=new Date;refreshExpiresAt.setDate(refreshExpiresAt.getDate()+refreshExpiresInDays);if(isPostgres)await db.unsafe(`
27
27
  INSERT INTO oauth_refresh_tokens (access_token_id, token, revoked, expires_at, created_at)
28
28
  VALUES ($1, $2, false, $3, ${appNow()})
29
29
  `,[accessToken.id,hashedRefreshToken,sqlDateTime(refreshExpiresAt)]);else await db.unsafe(`
@@ -53,7 +53,7 @@ import{createHash,randomBytes}from"node:crypto";import{db}from"@stacksjs/databas
53
53
  VALUES (?, ?, ?, ?, ?, 0, ?, ${appNow()}, ${appNow()})
54
54
  `,[refreshRow.user_id,refreshRow.oauth_client_id,hashedToken,refreshRow.name,refreshRow.scopes,sqlDateTime(expiresAt)]);const row=(await trx.unsafe(`
55
55
  SELECT * FROM oauth_access_tokens WHERE token = ${param(1)} LIMIT 1
56
- `,[hashedToken]))[0],accessToken={id:row.id,userId:row.user_id,clientId:row.oauth_client_id,name:row.name,scopes:parseScopes(row.scopes),revoked:!1,expiresAt,createdAt:new Date(row.created_at),updatedAt:new Date(row.updated_at)};if(isIssuedBeforePasswordChange(row.created_at,await getPasswordChangedAt(refreshRow.user_id,trx)))throw new HttpError(401,"Invalid or expired refresh token");const newRefreshTokenPlain=generateSecureToken(40),newHashedRefreshToken=hashToken(newRefreshTokenPlain),refreshExpiresAt=new Date;refreshExpiresAt.setDate(refreshExpiresAt.getDate()+refreshExpiresInDays);if(isPostgres)await trx.unsafe(`
56
+ `,[hashedToken]))[0];if(!row)throw new HttpError(500,"Failed to read back the access token that was just created.");const accessToken={id:row.id,userId:row.user_id,clientId:row.oauth_client_id,name:row.name||"access-token",scopes:parseScopes(row.scopes),revoked:!1,expiresAt,createdAt:new Date(row.created_at),updatedAt:row.updated_at?new Date(row.updated_at):new Date(row.created_at)};if(isIssuedBeforePasswordChange(row.created_at,await getPasswordChangedAt(refreshRow.user_id,trx)))throw new HttpError(401,"Invalid or expired refresh token");const newRefreshTokenPlain=generateSecureToken(40),newHashedRefreshToken=hashToken(newRefreshTokenPlain),refreshExpiresAt=new Date;refreshExpiresAt.setDate(refreshExpiresAt.getDate()+refreshExpiresInDays);if(isPostgres)await trx.unsafe(`
57
57
  INSERT INTO oauth_refresh_tokens (access_token_id, token, revoked, expires_at, created_at)
58
58
  VALUES ($1, $2, false, $3, ${appNow()})
59
59
  `,[accessToken.id,newHashedRefreshToken,sqlDateTime(refreshExpiresAt)]);else await trx.unsafe(`
@@ -75,13 +75,13 @@ import{createHash,randomBytes}from"node:crypto";import{db}from"@stacksjs/databas
75
75
  WHERE access_token_id IN (
76
76
  SELECT id FROM oauth_access_tokens WHERE user_id = ${param(1)}
77
77
  )
78
- `,[userId])}export async function deleteExpiredRefreshTokens(){const result=await db.unsafe(`
78
+ `,[userId])}export async function deleteExpiredRefreshTokens(){const written=await db.unsafe(`
79
79
  DELETE FROM oauth_refresh_tokens
80
80
  WHERE expires_at < ${appNow()}
81
- `);return result?.changes||result?.rowCount||0}export async function deleteRevokedRefreshTokens(daysOld=7){const cutoffDate=new Date;cutoffDate.setDate(cutoffDate.getDate()-daysOld);const result=await db.unsafe(`
81
+ `);return Number(written?.changes??written?.rowCount??0)}export async function deleteRevokedRefreshTokens(daysOld=7){const cutoffDate=new Date;cutoffDate.setDate(cutoffDate.getDate()-daysOld);const written=await db.unsafe(`
82
82
  DELETE FROM oauth_refresh_tokens
83
83
  WHERE revoked = ${boolTrue} AND created_at < ${param(1)}
84
- `,[sqlDateTime(cutoffDate)]);return result?.changes||result?.rowCount||0}export async function revokeToken(plainTextToken){const hashedToken=bearerLookupHash(plainTextToken);await db.unsafe(`
84
+ `,[sqlDateTime(cutoffDate)]);return Number(written?.changes??written?.rowCount??0)}export async function revokeToken(plainTextToken){const hashedToken=bearerLookupHash(plainTextToken);await db.unsafe(`
85
85
  UPDATE oauth_refresh_tokens
86
86
  SET revoked = ${boolTrue}
87
87
  WHERE access_token_id IN (
@@ -128,18 +128,18 @@ import{createHash,randomBytes}from"node:crypto";import{db}from"@stacksjs/databas
128
128
  WHERE access_token_id IN (
129
129
  SELECT id FROM oauth_access_tokens WHERE expires_at < ${appNow()}
130
130
  )
131
- `);const result=await db.unsafe(`
131
+ `);const written=await db.unsafe(`
132
132
  DELETE FROM oauth_access_tokens
133
133
  WHERE expires_at < ${appNow()}
134
- `);return result?.changes||result?.rowCount||0}export async function deleteRevokedTokens(daysOld=7){const cutoffDate=new Date;cutoffDate.setDate(cutoffDate.getDate()-daysOld);await db.unsafe(`
134
+ `);return Number(written?.changes??written?.rowCount??0)}export async function deleteRevokedTokens(daysOld=7){const cutoffDate=new Date;cutoffDate.setDate(cutoffDate.getDate()-daysOld);await db.unsafe(`
135
135
  DELETE FROM oauth_refresh_tokens
136
136
  WHERE access_token_id IN (
137
137
  SELECT id FROM oauth_access_tokens WHERE revoked = ${boolTrue} AND updated_at < ${param(1)}
138
138
  )
139
- `,[sqlDateTime(cutoffDate)]);const result=await db.unsafe(`
139
+ `,[sqlDateTime(cutoffDate)]);const written=await db.unsafe(`
140
140
  DELETE FROM oauth_access_tokens
141
141
  WHERE revoked = ${boolTrue} AND updated_at < ${param(1)}
142
- `,[sqlDateTime(cutoffDate)]);return result?.changes||result?.rowCount||0}export async function clients(userId){return(await db.unsafe(`
142
+ `,[sqlDateTime(cutoffDate)]);return Number(written?.changes??written?.rowCount??0)}export async function clients(userId){return(await db.unsafe(`
143
143
  SELECT * FROM oauth_clients
144
144
  WHERE user_id = ${param(1)} AND revoked = ${boolFalse}
145
145
  ORDER BY created_at DESC
@@ -151,9 +151,9 @@ import{createHash,randomBytes}from"node:crypto";import{db}from"@stacksjs/databas
151
151
  `,[options.name,secret,options.redirect,options.personalAccessClient||!1,options.passwordClient||!1]);else await db.unsafe(`
152
152
  INSERT INTO oauth_clients (name, secret, provider, redirect, personal_access_client, password_client, revoked, created_at)
153
153
  VALUES (?, ?, 'local', ?, ?, ?, 0, ${appNow()})
154
- `,[options.name,secret,options.redirect,options.personalAccessClient?1:0,options.passwordClient?1:0]);const inserted=await db.unsafe(`
154
+ `,[options.name,secret,options.redirect,options.personalAccessClient?1:0,options.passwordClient?1:0]);const createdClient=(await db.unsafe(`
155
155
  SELECT * FROM oauth_clients WHERE secret = ${param(1)} LIMIT 1
156
- `,[secret]);return{client:mapToOAuthClient(inserted[0]),plainTextSecret:secret}}export async function revokeClient(clientId){await db.unsafe(`
156
+ `,[secret]))[0];if(!createdClient)throw new HttpError(500,"Failed to read back the OAuth client that was just created.");return{client:mapToOAuthClient(createdClient),plainTextSecret:secret}}export async function revokeClient(clientId){await db.unsafe(`
157
157
  UPDATE oauth_clients
158
158
  SET revoked = ${boolTrue}, updated_at = ${appNow()}
159
159
  WHERE id = ${param(1)}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/auth",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.72.102",
5
+ "version": "0.73.0",
6
6
  "description": "A more simplistic way to authenticate.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -62,7 +62,7 @@
62
62
  },
63
63
  "devDependencies": {
64
64
  "better-dx": "^0.2.24",
65
- "@stacksjs/error-handling": "0.72.102",
66
- "@stacksjs/router": "0.72.102"
65
+ "@stacksjs/error-handling": "0.73.0",
66
+ "@stacksjs/router": "0.73.0"
67
67
  }
68
68
  }