@spfn/auth 0.3.0-beta.6 → 0.3.0-beta.7

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/README.md CHANGED
@@ -1178,6 +1178,70 @@ app.post('/v1/echo', createClientProofGuard(state), (c) => { /* handler */ });
1178
1178
  Responses and errors MUST be canonical bytes with the contract envelope — build them with
1179
1179
  `encodeCanonicalJson`/`ClientProofRefusal`, never `c.json()` (key order and int64 differ).
1180
1180
 
1181
+ ## Custom auth profiles (`registerAuthProfile`)
1182
+
1183
+ `clientProofV1` is not a special case in the middleware — it is one entry in a registry
1184
+ `authenticate` and `optionalAuth` dispatch on. An app registers its own scheme the same way,
1185
+ without forking the middleware or wrapping it:
1186
+
1187
+ ```typescript
1188
+ import { registerAuthProfile, type AuthContext } from '@spfn/auth/server';
1189
+ import { UnauthorizedError } from '@spfn/core/errors';
1190
+
1191
+ // At boot — server.config.ts, before the server starts taking requests.
1192
+ registerAuthProfile('serviceTokenV1', {
1193
+ verify: async (c): Promise<AuthContext> =>
1194
+ {
1195
+ const user = await findServiceAccount(c.req.header('x-acme-service-token'));
1196
+ if (user === null)
1197
+ {
1198
+ // A refusal leaves the verifier as a throw. It reaches the app's
1199
+ // error handler exactly as the Bearer path's does.
1200
+ throw new UnauthorizedError({ message: 'Invalid service token' });
1201
+ }
1202
+
1203
+ return {
1204
+ user,
1205
+ userId: String(user.id),
1206
+ keyId: 'service-token',
1207
+ role: null,
1208
+ locale: 'en',
1209
+ scheme: 'serviceTokenV1',
1210
+ };
1211
+ },
1212
+ });
1213
+ ```
1214
+
1215
+ A request naming the profile is then answered by that verifier:
1216
+
1217
+ ```http
1218
+ POST /v1/reports
1219
+ x-spfn-auth-profile: serviceTokenV1
1220
+ x-acme-service-token: <the app's own credential>
1221
+ ```
1222
+
1223
+ - **Register at boot, before the first request.** The registry is read on every dispatch, so a
1224
+ profile registered later is simply a profile the requests before it did not have. Registration
1225
+ is not frozen after startup — it is a contract, not a runtime check.
1226
+ - **A duplicate name throws**, `clientProofV1` included. Replacing a registered verifier silently
1227
+ is how an import order or a copied profile name swaps the code that decides who is admitted, so
1228
+ there is no override — and no unregistration API for the same reason.
1229
+ - **The verifier must expose a callable `verify`**, and what it resolves must carry a `userId` — a
1230
+ verifier that cannot admit anyone is refused at boot, and a resolve without a principal (`null`,
1231
+ the JS idiom for "no user") is refused as a throw rather than routed as authenticated.
1232
+ - **An unknown profile is still refused** (`PROFILE_REJECTED`, 400): registering one name does not
1233
+ open the header to others.
1234
+ - **Mixing is still refused.** A request carrying both `x-spfn-auth-profile` and `Authorization` is
1235
+ rejected before either path runs; a custom verifier never sees it.
1236
+ - **A verifier's throw propagates**, and only the internal clientProofV1 contract refusal is
1237
+ answered with the canonical envelope. Under `optionalAuth` too: credentials that were presented
1238
+ and refused are never downgraded to anonymous passage — only "presented nothing" continues
1239
+ without an auth context.
1240
+ - **`AuthContext.scheme` is an open union** — `'bearer' | 'clientProofV1' | 'oneTimeToken' | (string
1241
+ & {})`. The built-in names keep their autocomplete and a registered profile names its own scheme.
1242
+ The field stays informational: downstream permission and tenant code takes one principal shape and
1243
+ never branches on how it was produced.
1244
+
1181
1245
  ## Account Deletion & Recovery
1182
1246
 
1183
1247
  Grace-period deletion with in-window recovery, an admin/GDPR-response entry point for immediate
@@ -993,7 +993,7 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
993
993
  id: number;
994
994
  name: string;
995
995
  displayName: string;
996
- category: "auth" | "custom" | "user" | "rbac" | "system" | undefined;
996
+ category: "custom" | "user" | "auth" | "rbac" | "system" | undefined;
997
997
  }[];
998
998
  userId: number;
999
999
  publicId: string;
@@ -1293,8 +1293,8 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
1293
1293
  }, {}, {
1294
1294
  roles: {
1295
1295
  description: string | null;
1296
- name: string;
1297
1296
  id: number;
1297
+ name: string;
1298
1298
  displayName: string;
1299
1299
  isBuiltin: boolean;
1300
1300
  isSystem: boolean;
@@ -1315,8 +1315,8 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
1315
1315
  }, {}, {
1316
1316
  role: {
1317
1317
  description: string | null;
1318
- name: string;
1319
1318
  id: number;
1319
+ name: string;
1320
1320
  displayName: string;
1321
1321
  isBuiltin: boolean;
1322
1322
  isSystem: boolean;
@@ -1339,8 +1339,8 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
1339
1339
  }, {}, {
1340
1340
  role: {
1341
1341
  description: string | null;
1342
- name: string;
1343
1342
  id: number;
1343
+ name: string;
1344
1344
  displayName: string;
1345
1345
  isBuiltin: boolean;
1346
1346
  isSystem: boolean;
@@ -1424,6 +1424,11 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
1424
1424
  * downstream permission/tenant code consumes one principal shape and never
1425
1425
  * branches on how it was authenticated.
1426
1426
  *
1427
+ * An app adds its own scheme with `registerAuthProfile` at boot. The dispatch
1428
+ * it joins is the one below, unchanged: a name nobody registered is still
1429
+ * refused, and profile credentials mixed with an Authorization header are
1430
+ * still refused before either path runs.
1431
+ *
1427
1432
  * The clientProofV1 verifier reuses the phase-1 admission pieces (header
1428
1433
  * shape, canonical body, proof-input assembly, ECDSA verification) with two
1429
1434
  * production substitutions: the key directory is `user_public_keys` via
@@ -1444,8 +1449,13 @@ interface AuthContext {
1444
1449
  keyId: string;
1445
1450
  role: string | null;
1446
1451
  locale: string;
1447
- /** How the principal was authenticated. Informational — downstream code never branches on it. */
1448
- scheme: 'bearer' | 'clientProofV1' | 'oneTimeToken';
1452
+ /**
1453
+ * How the principal was authenticated. Informational — downstream code
1454
+ * never branches on it. The union stays open for the profiles an app
1455
+ * registers itself: the built-in names keep their autocomplete, and a
1456
+ * registered profile names its own scheme without editing this file.
1457
+ */
1458
+ scheme: 'bearer' | 'clientProofV1' | 'oneTimeToken' | (string & {});
1449
1459
  }
1450
1460
  /** A profile's verifier: admits the request and returns the principal, or throws. */
1451
1461
  interface AuthProfileVerifier {
@@ -1492,6 +1502,49 @@ type AuthProfileOutcome = {
1492
1502
  * handed to the generic error handler is classified by its class name instead.
1493
1503
  */
1494
1504
  declare function runAuthProfile(c: Context): Promise<AuthProfileOutcome>;
1505
+ /**
1506
+ * Registers an app's own verifier under a profile name.
1507
+ *
1508
+ * Call it at boot, before the first request: the registry is a module-global
1509
+ * read on every dispatch, so a profile registered later is simply a profile
1510
+ * the requests before it did not have. There is no freeze and no
1511
+ * unregistration — an auth surface that can be rearranged at runtime is a
1512
+ * surface an app bug can rearrange.
1513
+ *
1514
+ * A duplicate name throws rather than replacing the verifier that holds it,
1515
+ * `clientProofV1` included. A silent override is how a second import order, or
1516
+ * a copied profile name, quietly swaps the code that decides who is admitted.
1517
+ *
1518
+ * The verifier must expose a callable `verify` — a value that cannot admit
1519
+ * anyone is refused at boot rather than becoming a registry entry the dispatch
1520
+ * reads as "no profile header", which is anonymous passage under
1521
+ * `optionalAuth` for a request that presented profile credentials.
1522
+ *
1523
+ * The verifier returns the same `AuthContext` the Bearer path sets and refuses
1524
+ * by throwing. A resolve that carries no `userId` is refused as a throw too —
1525
+ * "no user" is a refusal, never a principal. A throw is not caught here:
1526
+ * `runAuthProfile` answers the internal clientProofV1 contract refusal and
1527
+ * nothing else, so a verifier's own error reaches the app's generic error
1528
+ * handler exactly as the Bearer path's `UnauthorizedError` does — and never
1529
+ * becomes anonymous passage, not even under `optionalAuth`.
1530
+ *
1531
+ * @example
1532
+ * ```typescript
1533
+ * registerAuthProfile('serviceTokenV1', {
1534
+ * verify: async (c) =>
1535
+ * {
1536
+ * const user = await authenticateServiceToken(c.req.header('x-acme-service-token'));
1537
+ * if (user === null)
1538
+ * {
1539
+ * throw new UnauthorizedError({ message: 'Invalid service token' });
1540
+ * }
1541
+ *
1542
+ * return { user, userId: String(user.id), keyId: 'service', role: null, locale: 'en', scheme: 'serviceTokenV1' };
1543
+ * },
1544
+ * });
1545
+ * ```
1546
+ */
1547
+ declare function registerAuthProfile(profileId: string, verifier: AuthProfileVerifier): void;
1495
1548
 
1496
1549
  declare module 'hono' {
1497
1550
  interface ContextVariableMap {
@@ -1558,4 +1611,4 @@ declare const authenticate: _spfn_core_route.NamedMiddleware<"auth">;
1558
1611
  */
1559
1612
  declare const optionalAuth: _spfn_core_route.NamedMiddleware<"optionalAuth">;
1560
1613
 
1561
- export { type SendVerificationCodeParams as $, type AuthInitOptions as A, type OAuthCodeExchangeOptions as B, type ConfirmSignupLinkResult as C, DeviceNameSchema as D, EmailSchema as E, type OAuthNativeParams as F, type OAuthStartParams as G, type OAuthTokens as H, type IssueOneTimeTokenResult as I, PasswordSchema as J, type KeySummary as K, type LoginResult as L, PhoneSchema as M, type NativeVerifyOptions as N, type OAuthStartResult as O, type PermissionConfig as P, PlatformSchema as Q, type RoleConfig as R, type SendVerificationCodeResult as S, type RegisterParams as T, type UserProfile as U, VERIFICATION_PURPOSES as V, type RegisterPublicKeyParams as W, type RequestSignupLinkParams as X, type RevokeAllKeysParams as Y, type RevokeKeyParams as Z, type RotateKeyParams as _, type RegisterResult as a, TargetTypeSchema as a0, type UnlinkNotification as a1, UnlinkNotifyRejection as a2, type UnlinkNotifyRequest as a3, type UnlinkNotifyResult as a4, VerificationPurposeSchema as a5, type VerifyCodeParams as a6, type VerifyCodeResult as a7, authenticate as a8, buildOAuthErrorUrl as a9, rotateKeyService as aA, runAuthProfile as aB, selectAuthProfile as aC, sendVerificationCodeService as aD, verifyCodeService as aE, verifyOneTimeTokenService as aF, changePasswordService as aa, completeSignupService as ab, confirmSignupLinkService as ac, getEnabledOAuthProviders as ad, getGoogleAccessToken as ae, getOAuthProvider as af, getRegisteredProviders as ag, isOAuthProviderEnabled as ah, isSafeReturnPath as ai, issueOneTimeTokenService as aj, listKeysService as ak, loginService as al, logoutService as am, oauthCallbackService as an, oauthNativeService as ao, oauthStartService as ap, oauthUnlinkNotifyService as aq, optionalAuth as ar, registerOAuthProvider as as, registerPublicKeyService as at, registerService as au, requestSignupLinkService as av, requireEnabledProvider as aw, resolveAuthenticatedUser as ax, revokeAllKeysService as ay, revokeKeyService as az, type RequestSignupLinkResult as b, type RotateKeyResult as c, type RevokeAllKeysResult as d, type OAuthNativeResult as e, type ProfileInfo as f, type AuthSession as g, PERMISSION_CATEGORIES as h, type PermissionCategory as i, VERIFICATION_TARGET_TYPES as j, type VerificationPurpose as k, type VerificationTargetType as l, mainAuthRouter as m, type OAuthProvider as n, type AuthContext as o, type AuthProfileOutcome as p, type AuthProfileVerifier as q, type ChangePasswordParams as r, type CompleteSignupParams as s, type ConfirmSignupLinkParams as t, KEY_FINGERPRINT_PREFIX_LENGTH as u, type LoginParams as v, type LogoutParams as w, type NormalizedIdentity as x, type OAuthCallbackParams as y, type OAuthCallbackResult as z };
1614
+ export { type SendVerificationCodeParams as $, type AuthInitOptions as A, type OAuthCodeExchangeOptions as B, type ConfirmSignupLinkResult as C, DeviceNameSchema as D, EmailSchema as E, type OAuthNativeParams as F, type OAuthStartParams as G, type OAuthTokens as H, type IssueOneTimeTokenResult as I, PasswordSchema as J, type KeySummary as K, type LoginResult as L, PhoneSchema as M, type NativeVerifyOptions as N, type OAuthStartResult as O, type PermissionConfig as P, PlatformSchema as Q, type RoleConfig as R, type SendVerificationCodeResult as S, type RegisterParams as T, type UserProfile as U, VERIFICATION_PURPOSES as V, type RegisterPublicKeyParams as W, type RequestSignupLinkParams as X, type RevokeAllKeysParams as Y, type RevokeKeyParams as Z, type RotateKeyParams as _, type RegisterResult as a, TargetTypeSchema as a0, type UnlinkNotification as a1, UnlinkNotifyRejection as a2, type UnlinkNotifyRequest as a3, type UnlinkNotifyResult as a4, VerificationPurposeSchema as a5, type VerifyCodeParams as a6, type VerifyCodeResult as a7, authenticate as a8, buildOAuthErrorUrl as a9, revokeKeyService as aA, rotateKeyService as aB, runAuthProfile as aC, selectAuthProfile as aD, sendVerificationCodeService as aE, verifyCodeService as aF, verifyOneTimeTokenService as aG, changePasswordService as aa, completeSignupService as ab, confirmSignupLinkService as ac, getEnabledOAuthProviders as ad, getGoogleAccessToken as ae, getOAuthProvider as af, getRegisteredProviders as ag, isOAuthProviderEnabled as ah, isSafeReturnPath as ai, issueOneTimeTokenService as aj, listKeysService as ak, loginService as al, logoutService as am, oauthCallbackService as an, oauthNativeService as ao, oauthStartService as ap, oauthUnlinkNotifyService as aq, optionalAuth as ar, registerAuthProfile as as, registerOAuthProvider as at, registerPublicKeyService as au, registerService as av, requestSignupLinkService as aw, requireEnabledProvider as ax, resolveAuthenticatedUser as ay, revokeAllKeysService as az, type RequestSignupLinkResult as b, type RotateKeyResult as c, type RevokeAllKeysResult as d, type OAuthNativeResult as e, type ProfileInfo as f, type AuthSession as g, PERMISSION_CATEGORIES as h, type PermissionCategory as i, VERIFICATION_TARGET_TYPES as j, type VerificationPurpose as k, type VerificationTargetType as l, mainAuthRouter as m, type OAuthProvider as n, type AuthContext as o, type AuthProfileOutcome as p, type AuthProfileVerifier as q, type ChangePasswordParams as r, type CompleteSignupParams as s, type ConfirmSignupLinkParams as t, KEY_FINGERPRINT_PREFIX_LENGTH as u, type LoginParams as v, type LogoutParams as w, type NormalizedIdentity as x, type OAuthCallbackParams as y, type OAuthCallbackResult as z };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _spfn_core_nextjs from '@spfn/core/nextjs';
2
- import { P as PermissionConfig, R as RoleConfig, m as mainAuthRouter, S as SendVerificationCodeResult, a as RegisterResult, b as RequestSignupLinkResult, C as ConfirmSignupLinkResult, L as LoginResult, c as RotateKeyResult, K as KeySummary, d as RevokeAllKeysResult, I as IssueOneTimeTokenResult, O as OAuthStartResult, e as OAuthNativeResult, U as UserProfile, f as ProfileInfo } from './authenticate-98lBIMxP.js';
3
- export { A as AuthInitOptions, g as AuthSession, h as PERMISSION_CATEGORIES, i as PermissionCategory, V as VERIFICATION_PURPOSES, j as VERIFICATION_TARGET_TYPES, k as VerificationPurpose, l as VerificationTargetType } from './authenticate-98lBIMxP.js';
2
+ import { P as PermissionConfig, R as RoleConfig, m as mainAuthRouter, S as SendVerificationCodeResult, a as RegisterResult, b as RequestSignupLinkResult, C as ConfirmSignupLinkResult, L as LoginResult, c as RotateKeyResult, K as KeySummary, d as RevokeAllKeysResult, I as IssueOneTimeTokenResult, O as OAuthStartResult, e as OAuthNativeResult, U as UserProfile, f as ProfileInfo } from './authenticate-Mg9D7Nys.js';
3
+ export { A as AuthInitOptions, g as AuthSession, h as PERMISSION_CATEGORIES, i as PermissionCategory, V as VERIFICATION_PURPOSES, j as VERIFICATION_TARGET_TYPES, k as VerificationPurpose, l as VerificationTargetType } from './authenticate-Mg9D7Nys.js';
4
4
  import * as _spfn_core_route from '@spfn/core/route';
5
5
  import { HttpMethod } from '@spfn/core/route';
6
6
  export { A as ACCOUNT_DELETION_REQUESTED_BY, a as ACCOUNT_DELETION_REQUEST_STATUSES, b as AccountDeletionRequestStatus, c as AccountDeletionRequestedBy, I as INVITATION_STATUSES, d as InvitationStatus, e as KEY_ALGORITHM, f as KEY_DEVICE_NAME_MAX_LENGTH, g as KEY_PLATFORM, K as KeyAlgorithmType, h as KeyPlatformType, P as PURGE_STRATEGIES, i as PurgeStrategy, S as SOCIAL_PROVIDERS, j as SocialProvider, U as USER_STATUSES, k as UserStatus } from './types-DYyhze28.js';
@@ -217,7 +217,7 @@ declare const authApi: _spfn_core_nextjs.Client<_spfn_core_route.Router<{
217
217
  id: number;
218
218
  name: string;
219
219
  displayName: string;
220
- category: "auth" | "custom" | "user" | "rbac" | "system" | undefined;
220
+ category: "custom" | "user" | "auth" | "rbac" | "system" | undefined;
221
221
  }[];
222
222
  userId: number;
223
223
  publicId: string;
@@ -517,8 +517,8 @@ declare const authApi: _spfn_core_nextjs.Client<_spfn_core_route.Router<{
517
517
  }, {}, {
518
518
  roles: {
519
519
  description: string | null;
520
- name: string;
521
520
  id: number;
521
+ name: string;
522
522
  displayName: string;
523
523
  isBuiltin: boolean;
524
524
  isSystem: boolean;
@@ -539,8 +539,8 @@ declare const authApi: _spfn_core_nextjs.Client<_spfn_core_route.Router<{
539
539
  }, {}, {
540
540
  role: {
541
541
  description: string | null;
542
- name: string;
543
542
  id: number;
543
+ name: string;
544
544
  displayName: string;
545
545
  isBuiltin: boolean;
546
546
  isSystem: boolean;
@@ -563,8 +563,8 @@ declare const authApi: _spfn_core_nextjs.Client<_spfn_core_route.Router<{
563
563
  }, {}, {
564
564
  role: {
565
565
  description: string | null;
566
- name: string;
567
566
  id: number;
567
+ name: string;
568
568
  displayName: string;
569
569
  isBuiltin: boolean;
570
570
  isSystem: boolean;