@iqauth/sdk 2.6.4 → 2.8.1

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 (117) hide show
  1. package/README.md +173 -1
  2. package/dist/browser-session.d.mts +4 -4
  3. package/dist/browser-session.d.ts +4 -4
  4. package/dist/browser-session.js +212 -46
  5. package/dist/browser-session.mjs +3 -3
  6. package/dist/browser.d.mts +5 -5
  7. package/dist/browser.d.ts +5 -5
  8. package/dist/browser.js +293 -34
  9. package/dist/browser.mjs +5 -5
  10. package/dist/{chunk-BVV54LPI.mjs → chunk-25SSYDIP.mjs} +10 -4
  11. package/dist/{chunk-XAWYUPMO.mjs → chunk-4V7FKOTG.mjs} +242 -22
  12. package/dist/{chunk-6I6RM4MN.mjs → chunk-6PJRLRB4.mjs} +33 -3
  13. package/dist/{chunk-SL3KRS4W.mjs → chunk-CIJORODR.mjs} +23 -1
  14. package/dist/{chunk-LIZYFXH7.mjs → chunk-DFWHSDYQ.mjs} +1 -1
  15. package/dist/chunk-GLXSIGVS.mjs +66 -0
  16. package/dist/{chunk-DJIBN2N7.mjs → chunk-GN37E64I.mjs} +29 -7
  17. package/dist/{chunk-WQWBJSSS.mjs → chunk-HVHNYPDC.mjs} +6 -6
  18. package/dist/chunk-JRDVUWAL.mjs +46 -0
  19. package/dist/{chunk-UNYDG2L4.mjs → chunk-NUO2I65G.mjs} +56 -23
  20. package/dist/{chunk-5T7GHBX6.mjs → chunk-TLET552H.mjs} +36 -0
  21. package/dist/chunk-VYQ3ETCK.mjs +244 -0
  22. package/dist/{chunk-3JULWS6F.mjs → chunk-WCELYTJ3.mjs} +3 -3
  23. package/dist/chunk-WHT6WKTY.mjs +3180 -0
  24. package/dist/{chunk-MKKZULZR.mjs → chunk-WIFG74IK.mjs} +1 -1
  25. package/dist/chunk-WSH4SW7F.mjs +490 -0
  26. package/dist/{chunk-W3F4JYGP.mjs → chunk-ZLJPABB7.mjs} +139 -23
  27. package/dist/cli/index.js +2 -2
  28. package/dist/cli/index.mjs +2 -2
  29. package/dist/{client-BNQe3AgF.d.ts → client-D8L-PaWr.d.mts} +59 -6
  30. package/dist/{client-kYlJFgPv.d.mts → client-DkPL0EPZ.d.ts} +59 -6
  31. package/dist/{doctor-YYNHNMLD.mjs → doctor-JAFXWU3X.mjs} +2 -2
  32. package/dist/errors-Jl1Jtm-6.d.mts +107 -0
  33. package/dist/errors-Jl1Jtm-6.d.ts +107 -0
  34. package/dist/{express-CHpfa7D_.d.ts → express-Budysq4h.d.ts} +2 -2
  35. package/dist/{express-B6_1vBYZ.d.mts → express-DDTA3qV1.d.mts} +2 -2
  36. package/dist/express.d.mts +7 -6
  37. package/dist/express.d.ts +7 -6
  38. package/dist/express.js +563 -85
  39. package/dist/express.mjs +73 -34
  40. package/dist/fastify.d.mts +10 -0
  41. package/dist/fastify.d.ts +10 -0
  42. package/dist/fastify.js +589 -65
  43. package/dist/fastify.mjs +101 -11
  44. package/dist/hono.d.mts +10 -0
  45. package/dist/hono.d.ts +10 -0
  46. package/dist/hono.js +566 -65
  47. package/dist/hono.mjs +78 -11
  48. package/dist/index-Cko-d5po.d.mts +1848 -0
  49. package/dist/index-RNqwEcmY.d.ts +1848 -0
  50. package/dist/index.d.mts +56 -8
  51. package/dist/index.d.ts +56 -8
  52. package/dist/index.js +694 -75
  53. package/dist/index.mjs +30 -10
  54. package/dist/{keys-NLWFAOEM.mjs → keys-6Y776TG2.mjs} +2 -2
  55. package/dist/locales.d.mts +1 -1
  56. package/dist/locales.d.ts +1 -1
  57. package/dist/locales.js +36 -0
  58. package/dist/locales.mjs +1 -1
  59. package/dist/mobile.d.mts +77 -7
  60. package/dist/mobile.d.ts +77 -7
  61. package/dist/mobile.js +307 -46
  62. package/dist/mobile.mjs +98 -3
  63. package/dist/next.d.mts +10 -1
  64. package/dist/next.d.ts +10 -1
  65. package/dist/next.js +596 -205
  66. package/dist/next.mjs +83 -10
  67. package/dist/{provisioningBridge-88xjOS2n.d.mts → provisioningBridge-BXPMZCLe.d.ts} +30 -2
  68. package/dist/{provisioningBridge-DnTfzdZK.d.ts → provisioningBridge-IEycmsgb.d.mts} +30 -2
  69. package/dist/{publishableKey-BaR0HoAH.d.ts → publishableKey-f2kq-rKw.d.mts} +1 -1
  70. package/dist/{publishableKey-BaR0HoAH.d.mts → publishableKey-f2kq-rKw.d.ts} +1 -1
  71. package/dist/react-permissions.d.mts +52 -0
  72. package/dist/react-permissions.d.ts +52 -0
  73. package/dist/react-permissions.js +239 -0
  74. package/dist/react-permissions.mjs +98 -0
  75. package/dist/react.d.mts +9 -1624
  76. package/dist/react.d.ts +9 -1624
  77. package/dist/react.js +882 -73
  78. package/dist/react.mjs +71 -2631
  79. package/dist/{reverify-4UEJXUS6.mjs → reverify-C64QXKJO.mjs} +2 -2
  80. package/dist/server/handlers.d.mts +200 -4
  81. package/dist/server/handlers.d.ts +200 -4
  82. package/dist/server/handlers.js +530 -16
  83. package/dist/server/handlers.mjs +14 -3
  84. package/dist/server.d.mts +171 -8
  85. package/dist/server.d.ts +171 -8
  86. package/dist/server.js +579 -61
  87. package/dist/server.mjs +99 -12
  88. package/dist/service.d.mts +4 -4
  89. package/dist/service.d.ts +4 -4
  90. package/dist/service.js +212 -46
  91. package/dist/service.mjs +3 -3
  92. package/dist/{signIn-CiIBTJIh.d.mts → signIn-CReqfXsh.d.mts} +95 -3
  93. package/dist/{signIn-OCr88Zf8.d.ts → signIn-Cfa1GTpO.d.ts} +95 -3
  94. package/dist/{signIn-4OKLDEIH.mjs → signIn-SHBW6Z4T.mjs} +1 -1
  95. package/dist/test.mjs +3 -3
  96. package/dist/{tokens-DCyzzn8L.d.mts → tokens-9F6ETrzk.d.ts} +9 -2
  97. package/dist/{tokens-aHiGFr_E.d.ts → tokens-B06VtvUi.d.mts} +9 -2
  98. package/dist/{types-DZAflmmq.d.mts → types-Bn8O-OEd.d.mts} +164 -11
  99. package/dist/{types-DZAflmmq.d.ts → types-Bn8O-OEd.d.ts} +164 -11
  100. package/dist/{types-6bNdxesb.d.ts → types-DnU2LhXR.d.mts} +7 -1
  101. package/dist/{types-6bNdxesb.d.mts → types-DnU2LhXR.d.ts} +7 -1
  102. package/dist/webhooks.d.mts +113 -17
  103. package/dist/webhooks.d.ts +113 -17
  104. package/dist/webhooks.js +179 -15
  105. package/dist/webhooks.mjs +7 -1
  106. package/dist/ws.d.mts +2 -2
  107. package/dist/ws.d.ts +2 -2
  108. package/dist/ws.js +80 -30
  109. package/dist/ws.mjs +4 -4
  110. package/docs/error-handling.md +101 -0
  111. package/docs/guides/effective-permissions.md +171 -0
  112. package/docs/guides/invitations.md +65 -0
  113. package/package.json +19 -4
  114. package/dist/chunk-6TDJJER7.mjs +0 -217
  115. package/dist/chunk-UKZLOHZG.mjs +0 -83
  116. package/dist/errors-CDdl24MP.d.mts +0 -52
  117. package/dist/errors-CDdl24MP.d.ts +0 -52
@@ -1,15 +1,26 @@
1
1
  import {
2
+ __resetSignoutMarkersForTests,
3
+ __resetSignoutRegistryWarningForTests,
4
+ buildUserinfoResponse,
5
+ createInMemorySignoutRegistry,
2
6
  handleCallback,
3
7
  handleRefresh,
4
8
  handleSignout,
9
+ handleUserinfo,
5
10
  serializeCookie
6
- } from "../chunk-6TDJJER7.mjs";
7
- import "../chunk-WQWBJSSS.mjs";
8
- import "../chunk-6I6RM4MN.mjs";
11
+ } from "../chunk-WSH4SW7F.mjs";
12
+ import "../chunk-HVHNYPDC.mjs";
13
+ import "../chunk-NUO2I65G.mjs";
14
+ import "../chunk-6PJRLRB4.mjs";
9
15
  import "../chunk-Y6FXYEAI.mjs";
10
16
  export {
17
+ __resetSignoutMarkersForTests,
18
+ __resetSignoutRegistryWarningForTests,
19
+ buildUserinfoResponse,
20
+ createInMemorySignoutRegistry,
11
21
  handleCallback,
12
22
  handleRefresh,
13
23
  handleSignout,
24
+ handleUserinfo,
14
25
  serializeCookie
15
26
  };
package/dist/server.d.mts CHANGED
@@ -1,10 +1,173 @@
1
- import { b as IQAuthTokenClientConfig, N as ExpressMiddlewareOptions, Q as IQAuthRequestLike, R as IQAuthResponseLike, V as IQAuthNextFunction } from './types-DZAflmmq.mjs';
2
- import { I as IQAuthClient } from './client-kYlJFgPv.mjs';
3
- export { E as ErrorCodes, I as IQAuthError } from './errors-CDdl24MP.mjs';
4
- export { C as CookieAwareMiddlewareOptions, D as DEFAULT_ACCESS_COOKIE, a as DEFAULT_REFRESH_COOKIE, i as iqAuthMiddleware } from './express-B6_1vBYZ.mjs';
5
- export { HandlerResponse, IQAuthHelperConfig, SetCookieDirective, handleCallback, handleRefresh, handleSignout, serializeCookie } from './server/handlers.mjs';
6
- export { P as ProvisioningBridge, a as ProvisioningBridgeOptions, d as ProvisioningContext, b as ProvisioningStorage, c as createProvisioningBridge } from './provisioningBridge-88xjOS2n.mjs';
7
- import './tokens-DCyzzn8L.mjs';
1
+ import { J as JwtClaims, f as IQAuthTokenClientConfig, X as ExpressMiddlewareOptions, a as IQAuthRequestLike, b as IQAuthResponseLike, c as IQAuthNextFunction } from './types-Bn8O-OEd.mjs';
2
+ import { I as IQAuthClient } from './client-D8L-PaWr.mjs';
3
+ export { E as ErrorCodes, I as IQAuthError } from './errors-Jl1Jtm-6.mjs';
4
+ export { C as CookieAwareMiddlewareOptions, D as DEFAULT_ACCESS_COOKIE, a as DEFAULT_REFRESH_COOKIE, i as iqAuthMiddleware } from './express-DDTA3qV1.mjs';
5
+ export { HandlerResponse, IQAuthHelperConfig, SetCookieDirective, UserinfoResponse, buildUserinfoResponse, handleCallback, handleRefresh, handleSignout, handleUserinfo, serializeCookie } from './server/handlers.mjs';
6
+ export { P as ProvisioningBridge, a as ProvisioningBridgeOptions, d as ProvisioningContext, e as ProvisioningError, b as ProvisioningStorage, c as createProvisioningBridge } from './provisioningBridge-IEycmsgb.mjs';
7
+ import './tokens-B06VtvUi.mjs';
8
+
9
+ /**
10
+ * linkLocalUserToIqAuthSub — first-time-migration helper.
11
+ *
12
+ * Use case: an app already had local users before adopting IQAuth. After a
13
+ * user signs in via IQAuth, the JWT's `sub` won't match any local row. The
14
+ * conventional self-heal is "look up by email, write the sub onto the matching
15
+ * row". Doing this safely requires:
16
+ *
17
+ * 1. An atomic transaction (find + write) so two concurrent first-time
18
+ * sign-ins for the same user can't both succeed.
19
+ * 2. Email matching that's case-insensitive by default (since email
20
+ * providers fold case in delivery).
21
+ * 3. A refusal to link when the candidate row already has a different
22
+ * non-null `iqauthSub` (i.e. that local row belongs to someone else).
23
+ * 4. A refusal to link when more than one local row matches the email
24
+ * (duplicate-email collision in the legacy table).
25
+ *
26
+ * The helper is ORM-agnostic. Pass any adapter that implements
27
+ * `LinkAdapter`. A reference Drizzle adapter is exported as
28
+ * `createDrizzleLinkAdapter`.
29
+ *
30
+ * Result codes:
31
+ * - 'linked' — wrote claims.sub onto the matched local row.
32
+ * - 'already_linked' — a row was already keyed by claims.sub. No write.
33
+ * - 'conflict' — matched row already has a different non-null sub,
34
+ * OR more than one local row matches the email,
35
+ * OR the email is unverified and adoption is gated
36
+ * (reason 'unverified_email' — see H-4 below).
37
+ * - 'not_found' — no local row matched any of the provided lookupBy
38
+ * keys. Caller should provision a new row separately.
39
+ *
40
+ * Security (H-4): writing the IQAuth `sub` onto a pre-existing local row matched
41
+ * by email is a takeover of that account. It is only performed when the claims
42
+ * assert a verified email (`claims.email_verified === true`). When the email is
43
+ * unverified and `allowUnverifiedEmail` is not set, the helper fails closed with
44
+ * `{ status: 'conflict', reason: 'unverified_email' }` instead of linking.
45
+ */
46
+
47
+ type LinkLookupBy = "email";
48
+ type LinkResult = {
49
+ status: "linked";
50
+ userId: string;
51
+ } | {
52
+ status: "already_linked";
53
+ userId: string;
54
+ } | {
55
+ status: "conflict";
56
+ userId?: string;
57
+ reason: "different_sub" | "duplicate_email" | "unverified_email";
58
+ } | {
59
+ status: "not_found";
60
+ };
61
+ interface LinkCandidate {
62
+ id: string;
63
+ email: string | null;
64
+ iqauthSub: string | null;
65
+ }
66
+ interface LinkAdapterTx {
67
+ findByIqAuthSub(sub: string): Promise<LinkCandidate | null>;
68
+ findByEmail(email: string, opts: {
69
+ caseInsensitive: boolean;
70
+ }): Promise<LinkCandidate[]>;
71
+ /**
72
+ * Write the IQAuth `sub` onto the matched row. Implementations SHOULD make
73
+ * this a conditional update (`WHERE id=? AND iqauth_sub IS NULL OR iqauth_sub=?`)
74
+ * and return `false` if no row was updated — that signals a concurrent
75
+ * writer claimed the row first and the helper will surface a `conflict`.
76
+ * For backward-compatibility, returning `void` is treated as "succeeded".
77
+ */
78
+ setIqAuthSub(userId: string, sub: string): Promise<void | boolean>;
79
+ }
80
+ interface LinkAdapter {
81
+ /**
82
+ * Run `fn` inside a serialized transaction. Implementations MUST guarantee
83
+ * that candidate rows surfaced by `findByIqAuthSub` / `findByEmail` are
84
+ * held under a row-level lock (or equivalent isolation) for the duration
85
+ * of the transaction so the read+write pair is atomic against other
86
+ * concurrent first-time logins for the same user. Concretely:
87
+ * - Postgres / MySQL: implement the read with `SELECT … FOR UPDATE`.
88
+ * - SQLite: relies on the file-level write lock — no extra clause needed.
89
+ * - Otherwise: run the transaction at SERIALIZABLE isolation.
90
+ * The reference `createDrizzleLinkAdapter` does this for you.
91
+ */
92
+ withTransaction<T>(fn: (tx: LinkAdapterTx) => Promise<T>): Promise<T>;
93
+ }
94
+ interface LinkLocalUserOptions {
95
+ adapter: LinkAdapter;
96
+ claims: Pick<JwtClaims, "sub" | "email" | "email_verified">;
97
+ /**
98
+ * Lookup keys to try, in order. Currently only `'email'` is supported.
99
+ * Defaults to `['email']`.
100
+ */
101
+ lookupBy?: LinkLookupBy[];
102
+ /**
103
+ * Email matching. Default true — most legacy stores fold case in delivery
104
+ * but preserve the cased original at registration time.
105
+ */
106
+ caseInsensitiveEmail?: boolean;
107
+ /**
108
+ * Security gate (H-4): by default the email→sub link only proceeds when the
109
+ * claims assert a verified email (`claims.email_verified === true`). When the
110
+ * email is unverified and this flag is left `false`, the helper fails closed
111
+ * with `{ status: 'conflict', reason: 'unverified_email' }` rather than
112
+ * letting an unverified email take over a pre-existing local account.
113
+ *
114
+ * Set `true` ONLY when your issuer is trusted to never emit an unverified
115
+ * email for linking (or you have a compensating control). Defaults to
116
+ * `false` (secure).
117
+ */
118
+ allowUnverifiedEmail?: boolean;
119
+ }
120
+ declare function linkLocalUserToIqAuthSub(options: LinkLocalUserOptions): Promise<LinkResult>;
121
+ interface DrizzleLikeDb {
122
+ transaction<T>(fn: (tx: unknown) => Promise<T>): Promise<T>;
123
+ }
124
+ interface DrizzleLinkAdapterDeps {
125
+ /** Drizzle DB instance with `.transaction()`. */
126
+ db: DrizzleLikeDb;
127
+ /** Drizzle table object (e.g. `users`). */
128
+ table: unknown;
129
+ /** Column refs on the table — `id`, `email`, and the iqauth sub column. */
130
+ columns: {
131
+ id: unknown;
132
+ email: unknown;
133
+ iqauthSub: unknown;
134
+ };
135
+ /**
136
+ * Optional override for the schema property keys used by `.set(...)` on
137
+ * UPDATE. Drizzle's `.set()` is keyed by the table's TypeScript property
138
+ * names (the keys of your schema object), which need not match the
139
+ * underlying column name. Defaults to `{ iqauthSub: 'iqauthSub' }`.
140
+ */
141
+ columnNames?: {
142
+ iqauthSub?: string;
143
+ };
144
+ /** Drizzle `eq` operator import (`drizzle-orm`). */
145
+ eq: (a: unknown, b: unknown) => unknown;
146
+ /**
147
+ * Drizzle `sql` template tag (`drizzle-orm`). Used for case-insensitive
148
+ * email comparison via `lower(email) = lower($1)`.
149
+ */
150
+ sql: (strings: TemplateStringsArray, ...values: unknown[]) => unknown;
151
+ }
152
+ /**
153
+ * Reference adapter for Drizzle ORM (Postgres / SQLite / MySQL). The host
154
+ * app passes its own `db`, `table`, column refs, and `eq` / `sql` imports
155
+ * so this SDK has no hard dependency on `drizzle-orm`.
156
+ *
157
+ * Example:
158
+ *
159
+ * import { eq, sql } from 'drizzle-orm';
160
+ * import { db } from './db';
161
+ * import { users } from './schema';
162
+ * import { createDrizzleLinkAdapter, linkLocalUserToIqAuthSub } from '@iqauth/sdk/server';
163
+ *
164
+ * const adapter = createDrizzleLinkAdapter({
165
+ * db, table: users, eq, sql,
166
+ * columns: { id: users.id, email: users.email, iqauthSub: users.iqauthSub },
167
+ * });
168
+ * const result = await linkLocalUserToIqAuthSub({ adapter, claims: req.auth });
169
+ */
170
+ declare function createDrizzleLinkAdapter(deps: DrizzleLinkAdapterDeps): LinkAdapter;
8
171
 
9
172
  declare class ServerIQAuthClient extends IQAuthClient {
10
173
  constructor(config: IQAuthTokenClientConfig);
@@ -12,4 +175,4 @@ declare class ServerIQAuthClient extends IQAuthClient {
12
175
  }
13
176
  declare function createServerClient(config: IQAuthTokenClientConfig): ServerIQAuthClient;
14
177
 
15
- export { ExpressMiddlewareOptions, IQAuthClient, IQAuthTokenClientConfig, ServerIQAuthClient, createServerClient };
178
+ export { type DrizzleLinkAdapterDeps, ExpressMiddlewareOptions, IQAuthClient, IQAuthTokenClientConfig, type LinkAdapter, type LinkAdapterTx, type LinkCandidate, type LinkLocalUserOptions, type LinkLookupBy, type LinkResult, ServerIQAuthClient, createDrizzleLinkAdapter, createServerClient, linkLocalUserToIqAuthSub };
package/dist/server.d.ts CHANGED
@@ -1,10 +1,173 @@
1
- import { b as IQAuthTokenClientConfig, N as ExpressMiddlewareOptions, Q as IQAuthRequestLike, R as IQAuthResponseLike, V as IQAuthNextFunction } from './types-DZAflmmq.js';
2
- import { I as IQAuthClient } from './client-BNQe3AgF.js';
3
- export { E as ErrorCodes, I as IQAuthError } from './errors-CDdl24MP.js';
4
- export { C as CookieAwareMiddlewareOptions, D as DEFAULT_ACCESS_COOKIE, a as DEFAULT_REFRESH_COOKIE, i as iqAuthMiddleware } from './express-CHpfa7D_.js';
5
- export { HandlerResponse, IQAuthHelperConfig, SetCookieDirective, handleCallback, handleRefresh, handleSignout, serializeCookie } from './server/handlers.js';
6
- export { P as ProvisioningBridge, a as ProvisioningBridgeOptions, d as ProvisioningContext, b as ProvisioningStorage, c as createProvisioningBridge } from './provisioningBridge-DnTfzdZK.js';
7
- import './tokens-aHiGFr_E.js';
1
+ import { J as JwtClaims, f as IQAuthTokenClientConfig, X as ExpressMiddlewareOptions, a as IQAuthRequestLike, b as IQAuthResponseLike, c as IQAuthNextFunction } from './types-Bn8O-OEd.js';
2
+ import { I as IQAuthClient } from './client-DkPL0EPZ.js';
3
+ export { E as ErrorCodes, I as IQAuthError } from './errors-Jl1Jtm-6.js';
4
+ export { C as CookieAwareMiddlewareOptions, D as DEFAULT_ACCESS_COOKIE, a as DEFAULT_REFRESH_COOKIE, i as iqAuthMiddleware } from './express-Budysq4h.js';
5
+ export { HandlerResponse, IQAuthHelperConfig, SetCookieDirective, UserinfoResponse, buildUserinfoResponse, handleCallback, handleRefresh, handleSignout, handleUserinfo, serializeCookie } from './server/handlers.js';
6
+ export { P as ProvisioningBridge, a as ProvisioningBridgeOptions, d as ProvisioningContext, e as ProvisioningError, b as ProvisioningStorage, c as createProvisioningBridge } from './provisioningBridge-BXPMZCLe.js';
7
+ import './tokens-9F6ETrzk.js';
8
+
9
+ /**
10
+ * linkLocalUserToIqAuthSub — first-time-migration helper.
11
+ *
12
+ * Use case: an app already had local users before adopting IQAuth. After a
13
+ * user signs in via IQAuth, the JWT's `sub` won't match any local row. The
14
+ * conventional self-heal is "look up by email, write the sub onto the matching
15
+ * row". Doing this safely requires:
16
+ *
17
+ * 1. An atomic transaction (find + write) so two concurrent first-time
18
+ * sign-ins for the same user can't both succeed.
19
+ * 2. Email matching that's case-insensitive by default (since email
20
+ * providers fold case in delivery).
21
+ * 3. A refusal to link when the candidate row already has a different
22
+ * non-null `iqauthSub` (i.e. that local row belongs to someone else).
23
+ * 4. A refusal to link when more than one local row matches the email
24
+ * (duplicate-email collision in the legacy table).
25
+ *
26
+ * The helper is ORM-agnostic. Pass any adapter that implements
27
+ * `LinkAdapter`. A reference Drizzle adapter is exported as
28
+ * `createDrizzleLinkAdapter`.
29
+ *
30
+ * Result codes:
31
+ * - 'linked' — wrote claims.sub onto the matched local row.
32
+ * - 'already_linked' — a row was already keyed by claims.sub. No write.
33
+ * - 'conflict' — matched row already has a different non-null sub,
34
+ * OR more than one local row matches the email,
35
+ * OR the email is unverified and adoption is gated
36
+ * (reason 'unverified_email' — see H-4 below).
37
+ * - 'not_found' — no local row matched any of the provided lookupBy
38
+ * keys. Caller should provision a new row separately.
39
+ *
40
+ * Security (H-4): writing the IQAuth `sub` onto a pre-existing local row matched
41
+ * by email is a takeover of that account. It is only performed when the claims
42
+ * assert a verified email (`claims.email_verified === true`). When the email is
43
+ * unverified and `allowUnverifiedEmail` is not set, the helper fails closed with
44
+ * `{ status: 'conflict', reason: 'unverified_email' }` instead of linking.
45
+ */
46
+
47
+ type LinkLookupBy = "email";
48
+ type LinkResult = {
49
+ status: "linked";
50
+ userId: string;
51
+ } | {
52
+ status: "already_linked";
53
+ userId: string;
54
+ } | {
55
+ status: "conflict";
56
+ userId?: string;
57
+ reason: "different_sub" | "duplicate_email" | "unverified_email";
58
+ } | {
59
+ status: "not_found";
60
+ };
61
+ interface LinkCandidate {
62
+ id: string;
63
+ email: string | null;
64
+ iqauthSub: string | null;
65
+ }
66
+ interface LinkAdapterTx {
67
+ findByIqAuthSub(sub: string): Promise<LinkCandidate | null>;
68
+ findByEmail(email: string, opts: {
69
+ caseInsensitive: boolean;
70
+ }): Promise<LinkCandidate[]>;
71
+ /**
72
+ * Write the IQAuth `sub` onto the matched row. Implementations SHOULD make
73
+ * this a conditional update (`WHERE id=? AND iqauth_sub IS NULL OR iqauth_sub=?`)
74
+ * and return `false` if no row was updated — that signals a concurrent
75
+ * writer claimed the row first and the helper will surface a `conflict`.
76
+ * For backward-compatibility, returning `void` is treated as "succeeded".
77
+ */
78
+ setIqAuthSub(userId: string, sub: string): Promise<void | boolean>;
79
+ }
80
+ interface LinkAdapter {
81
+ /**
82
+ * Run `fn` inside a serialized transaction. Implementations MUST guarantee
83
+ * that candidate rows surfaced by `findByIqAuthSub` / `findByEmail` are
84
+ * held under a row-level lock (or equivalent isolation) for the duration
85
+ * of the transaction so the read+write pair is atomic against other
86
+ * concurrent first-time logins for the same user. Concretely:
87
+ * - Postgres / MySQL: implement the read with `SELECT … FOR UPDATE`.
88
+ * - SQLite: relies on the file-level write lock — no extra clause needed.
89
+ * - Otherwise: run the transaction at SERIALIZABLE isolation.
90
+ * The reference `createDrizzleLinkAdapter` does this for you.
91
+ */
92
+ withTransaction<T>(fn: (tx: LinkAdapterTx) => Promise<T>): Promise<T>;
93
+ }
94
+ interface LinkLocalUserOptions {
95
+ adapter: LinkAdapter;
96
+ claims: Pick<JwtClaims, "sub" | "email" | "email_verified">;
97
+ /**
98
+ * Lookup keys to try, in order. Currently only `'email'` is supported.
99
+ * Defaults to `['email']`.
100
+ */
101
+ lookupBy?: LinkLookupBy[];
102
+ /**
103
+ * Email matching. Default true — most legacy stores fold case in delivery
104
+ * but preserve the cased original at registration time.
105
+ */
106
+ caseInsensitiveEmail?: boolean;
107
+ /**
108
+ * Security gate (H-4): by default the email→sub link only proceeds when the
109
+ * claims assert a verified email (`claims.email_verified === true`). When the
110
+ * email is unverified and this flag is left `false`, the helper fails closed
111
+ * with `{ status: 'conflict', reason: 'unverified_email' }` rather than
112
+ * letting an unverified email take over a pre-existing local account.
113
+ *
114
+ * Set `true` ONLY when your issuer is trusted to never emit an unverified
115
+ * email for linking (or you have a compensating control). Defaults to
116
+ * `false` (secure).
117
+ */
118
+ allowUnverifiedEmail?: boolean;
119
+ }
120
+ declare function linkLocalUserToIqAuthSub(options: LinkLocalUserOptions): Promise<LinkResult>;
121
+ interface DrizzleLikeDb {
122
+ transaction<T>(fn: (tx: unknown) => Promise<T>): Promise<T>;
123
+ }
124
+ interface DrizzleLinkAdapterDeps {
125
+ /** Drizzle DB instance with `.transaction()`. */
126
+ db: DrizzleLikeDb;
127
+ /** Drizzle table object (e.g. `users`). */
128
+ table: unknown;
129
+ /** Column refs on the table — `id`, `email`, and the iqauth sub column. */
130
+ columns: {
131
+ id: unknown;
132
+ email: unknown;
133
+ iqauthSub: unknown;
134
+ };
135
+ /**
136
+ * Optional override for the schema property keys used by `.set(...)` on
137
+ * UPDATE. Drizzle's `.set()` is keyed by the table's TypeScript property
138
+ * names (the keys of your schema object), which need not match the
139
+ * underlying column name. Defaults to `{ iqauthSub: 'iqauthSub' }`.
140
+ */
141
+ columnNames?: {
142
+ iqauthSub?: string;
143
+ };
144
+ /** Drizzle `eq` operator import (`drizzle-orm`). */
145
+ eq: (a: unknown, b: unknown) => unknown;
146
+ /**
147
+ * Drizzle `sql` template tag (`drizzle-orm`). Used for case-insensitive
148
+ * email comparison via `lower(email) = lower($1)`.
149
+ */
150
+ sql: (strings: TemplateStringsArray, ...values: unknown[]) => unknown;
151
+ }
152
+ /**
153
+ * Reference adapter for Drizzle ORM (Postgres / SQLite / MySQL). The host
154
+ * app passes its own `db`, `table`, column refs, and `eq` / `sql` imports
155
+ * so this SDK has no hard dependency on `drizzle-orm`.
156
+ *
157
+ * Example:
158
+ *
159
+ * import { eq, sql } from 'drizzle-orm';
160
+ * import { db } from './db';
161
+ * import { users } from './schema';
162
+ * import { createDrizzleLinkAdapter, linkLocalUserToIqAuthSub } from '@iqauth/sdk/server';
163
+ *
164
+ * const adapter = createDrizzleLinkAdapter({
165
+ * db, table: users, eq, sql,
166
+ * columns: { id: users.id, email: users.email, iqauthSub: users.iqauthSub },
167
+ * });
168
+ * const result = await linkLocalUserToIqAuthSub({ adapter, claims: req.auth });
169
+ */
170
+ declare function createDrizzleLinkAdapter(deps: DrizzleLinkAdapterDeps): LinkAdapter;
8
171
 
9
172
  declare class ServerIQAuthClient extends IQAuthClient {
10
173
  constructor(config: IQAuthTokenClientConfig);
@@ -12,4 +175,4 @@ declare class ServerIQAuthClient extends IQAuthClient {
12
175
  }
13
176
  declare function createServerClient(config: IQAuthTokenClientConfig): ServerIQAuthClient;
14
177
 
15
- export { ExpressMiddlewareOptions, IQAuthClient, IQAuthTokenClientConfig, ServerIQAuthClient, createServerClient };
178
+ export { type DrizzleLinkAdapterDeps, ExpressMiddlewareOptions, IQAuthClient, IQAuthTokenClientConfig, type LinkAdapter, type LinkAdapterTx, type LinkCandidate, type LinkLocalUserOptions, type LinkLookupBy, type LinkResult, ServerIQAuthClient, createDrizzleLinkAdapter, createServerClient, linkLocalUserToIqAuthSub };