@guettonenrique/passwordless-auth 0.0.0-stage → 0.1.2
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/LICENSE +21 -0
- package/README.md +387 -2
- package/dist/functions/index.cjs +391 -0
- package/dist/functions/index.cjs.map +1 -0
- package/dist/functions/index.d.cts +70 -0
- package/dist/functions/index.d.ts +70 -0
- package/dist/functions/index.js +348 -0
- package/dist/functions/index.js.map +1 -0
- package/dist/react-native/index.cjs +617 -0
- package/dist/react-native/index.cjs.map +1 -0
- package/dist/react-native/index.d.cts +64 -0
- package/dist/react-native/index.d.ts +64 -0
- package/dist/react-native/index.js +606 -0
- package/dist/react-native/index.js.map +1 -0
- package/dist/shared/index.cjs +151 -0
- package/dist/shared/index.cjs.map +1 -0
- package/dist/shared/index.d.cts +50 -0
- package/dist/shared/index.d.ts +50 -0
- package/dist/shared/index.js +111 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/types-BUi8ZPam.d.cts +110 -0
- package/dist/types-BUi8ZPam.d.ts +110 -0
- package/package.json +91 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HouseVenture
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,388 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @guettonenrique/passwordless-auth
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Passwordless **email OTP** authentication in one package with three entry points:
|
|
4
|
+
|
|
5
|
+
- **`/functions`** — ready-to-register Firebase Cloud Function factories for
|
|
6
|
+
sending codes, verifying codes, and scheduled cleanup.
|
|
7
|
+
- **`/react-native`** — the mobile API layer plus `OtpInput` (auto-fill code
|
|
8
|
+
boxes) and `EmailOtpModal` (the full two-step dialog).
|
|
9
|
+
- **`/shared`** — the single source of truth for constants, types, validation,
|
|
10
|
+
and the email template. Pure TypeScript with no SDK imports.
|
|
11
|
+
|
|
12
|
+
The React Native entry point is optional. The backend functions can be used
|
|
13
|
+
without installing the package in the client, allowing the app to own the
|
|
14
|
+
email form, OTP input, modal, styling, resend timer, and error handling.
|
|
15
|
+
|
|
16
|
+
The security model is baked in and tested: SHA-256 hashed codes (never
|
|
17
|
+
plaintext), timing-safe comparison, single-use codes deleted inside the verify
|
|
18
|
+
transaction, a five-attempt brute-force cap, a 60-second resend cooldown,
|
|
19
|
+
10-minute expiry, and client-side access to the OTP collection blocked by
|
|
20
|
+
rules.
|
|
21
|
+
|
|
22
|
+
> **v0.1** — first publish. API may still change; breaking changes allowed until 1.0.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Package overview
|
|
27
|
+
|
|
28
|
+
### Backend (`/functions`)
|
|
29
|
+
|
|
30
|
+
Three factory functions return ready-to-deploy Firebase Cloud Functions:
|
|
31
|
+
|
|
32
|
+
| Factory | What it handles |
|
|
33
|
+
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
34
|
+
| `createRequestEmailOtp` | Email validation, 60 s resend cooldown, cryptographically secure 6-digit code generation, SHA-256 hashing (`email:code`), Firestore write, email dispatch (SendGrid default or custom sender), structured error responses |
|
|
35
|
+
| `createVerifyEmailOtp` | Code lookup, timing-safe hash comparison, expiry check, 5-attempt brute-force cap, single-use deletion inside a Firestore transaction, user provisioning (delegated to your hook), Firebase custom token minting |
|
|
36
|
+
| `createCleanupExpiredOtps` | Scheduled (every 15 min) drain-loop that deletes orphaned OTP docs (users who requested but never entered a code) |
|
|
37
|
+
|
|
38
|
+
### Mobile (`/react-native`)
|
|
39
|
+
|
|
40
|
+
The mobile entry point provides the API layer and UI components:
|
|
41
|
+
|
|
42
|
+
| Export | What it does |
|
|
43
|
+
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
44
|
+
| `requestEmailOtp(email)` | Calls the backend, returns `{ status, message }` |
|
|
45
|
+
| `verifyEmailOtp(email, code)` | Calls the backend, returns the Firebase custom token |
|
|
46
|
+
| `signInWithEmailOtp(email, code)` | Verifies + `signInWithCustomToken` — one call, user is signed in |
|
|
47
|
+
| `<OtpInput />` | 6-slot auto-fill code input (paste, iOS `oneTimeCode` / Android `one-time-code`, haptics, auto-submit on completion) |
|
|
48
|
+
| `<EmailOtpModal />` | Full two-step dialog (email → code) with cooldown countdown, resend, error banner, double-submit guard. On success the modal closes and your existing `onAuthStateChanged` listener takes over |
|
|
49
|
+
|
|
50
|
+
### Shared (`/shared`)
|
|
51
|
+
|
|
52
|
+
Constants, types, email template, and validation utilities. This entry point
|
|
53
|
+
is pure TypeScript with no SDK imports, keeping the backend and mobile
|
|
54
|
+
contracts in sync.
|
|
55
|
+
|
|
56
|
+
### Security model
|
|
57
|
+
|
|
58
|
+
- Codes are SHA-256 hashed before storage — plaintext never touches Firestore
|
|
59
|
+
- Timing-safe comparison prevents hash-timing attacks
|
|
60
|
+
- Single-use: the doc is deleted inside the verify transaction (concurrent verifies → one winner)
|
|
61
|
+
- 5-attempt cap per code, then the doc is destroyed
|
|
62
|
+
- 60-second resend cooldown, 10-minute code expiry
|
|
63
|
+
- Client-side access to the OTP collection is blocked by Firestore rules (only admin SDK writes)
|
|
64
|
+
- App Check enforcement is a one-flag option (`enforceAppCheck: true`)
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Quick start
|
|
69
|
+
|
|
70
|
+
### Backend
|
|
71
|
+
|
|
72
|
+
1. **Install** the package (see below).
|
|
73
|
+
2. **Register the three functions** — ~30 lines total:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// functions/src/index.ts (or per-function files)
|
|
77
|
+
import {
|
|
78
|
+
createRequestEmailOtp,
|
|
79
|
+
createVerifyEmailOtp,
|
|
80
|
+
createCleanupExpiredOtps,
|
|
81
|
+
} from "@guettonenrique/passwordless-auth/functions";
|
|
82
|
+
|
|
83
|
+
exports.requestEmailOtp = createRequestEmailOtp({
|
|
84
|
+
appName: "MyApp",
|
|
85
|
+
sendgrid: {
|
|
86
|
+
apiKey: defineSecret("SENDGRID_API_KEY"),
|
|
87
|
+
fromEmail,
|
|
88
|
+
fromName,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
exports.verifyEmailOtp = createVerifyEmailOtp({
|
|
93
|
+
provision: async (email) => {
|
|
94
|
+
// Your user schema: resolve or create, return the uid.
|
|
95
|
+
const { user } = await resolveOrCreateUser(email, {
|
|
96
|
+
emailVerified: true,
|
|
97
|
+
});
|
|
98
|
+
return user.uid;
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
exports.cleanupExpiredOtps = createCleanupExpiredOtps();
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The **only required customization** is the `provision` hook — it owns your user schema (Firestore docs, subscribers, notification settings, etc.). Everything else is config or defaults.
|
|
106
|
+
|
|
107
|
+
3. **Lock down the OTP collection** in `firestore.rules`:
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
match /auth_otps/{document=**} {
|
|
111
|
+
allow read, write: if false;
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
4. **Deploy**: `firebase deploy --only functions,firestore:rules`
|
|
116
|
+
|
|
117
|
+
### Mobile
|
|
118
|
+
|
|
119
|
+
1. **Install** the package + the mobile peers (see table below).
|
|
120
|
+
2. **Add the modal** to your auth screen — ~10 lines:
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
import { EmailOtpModal } from '@guettonenrique/passwordless-auth/react-native';
|
|
124
|
+
|
|
125
|
+
const [otpVisible, setOtpVisible] = useState(false);
|
|
126
|
+
|
|
127
|
+
<TouchableOpacity onPress={() => setOtpVisible(true)}>
|
|
128
|
+
<Text>Continue with email code</Text>
|
|
129
|
+
</TouchableOpacity>
|
|
130
|
+
|
|
131
|
+
<EmailOtpModal visible={otpVisible} onClose={() => setOtpVisible(false)} />
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
That's the entire integration. On success the modal closes and your existing `onAuthStateChanged` listener flips the app into the signed-in state — no navigation code needed.
|
|
135
|
+
|
|
136
|
+
3. **Optional:** use `OtpInput` + the API functions directly if you want to build a custom dialog.
|
|
137
|
+
|
|
138
|
+
The package never touches your auth state, navigation, user schema, or email
|
|
139
|
+
infrastructure beyond what you explicitly configure.
|
|
140
|
+
|
|
141
|
+
### Client-owned UI (without `/react-native`)
|
|
142
|
+
|
|
143
|
+
You can omit the package from the client entirely and call the deployed
|
|
144
|
+
Firebase callable functions with the app's existing Firebase dependencies.
|
|
145
|
+
The client flow is:
|
|
146
|
+
|
|
147
|
+
1. Call `requestEmailOtp` with the normalized email.
|
|
148
|
+
2. Collect the six-digit code in your own UI.
|
|
149
|
+
3. Call `verifyEmailOtp` with the email and code.
|
|
150
|
+
4. Read `customToken` from the response.
|
|
151
|
+
5. Sign in with Firebase Auth using `signInWithCustomToken`.
|
|
152
|
+
|
|
153
|
+
With React Native Firebase, the direct implementation looks like this:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { getAuth, signInWithCustomToken } from "@react-native-firebase/auth";
|
|
157
|
+
import { getFunctions, httpsCallable } from "@react-native-firebase/functions";
|
|
158
|
+
|
|
159
|
+
type VerifyOtpResponse = { customToken: string };
|
|
160
|
+
|
|
161
|
+
const requestEmailOtp = httpsCallable<
|
|
162
|
+
{ email: string },
|
|
163
|
+
{ status: string; message: string }
|
|
164
|
+
>(getFunctions(), "requestEmailOtp");
|
|
165
|
+
const verifyEmailOtp = httpsCallable<
|
|
166
|
+
{ email: string; code: string },
|
|
167
|
+
VerifyOtpResponse
|
|
168
|
+
>(getFunctions(), "verifyEmailOtp");
|
|
169
|
+
|
|
170
|
+
await requestEmailOtp({ email: email.trim().toLowerCase() });
|
|
171
|
+
|
|
172
|
+
const result = await verifyEmailOtp({
|
|
173
|
+
email: email.trim().toLowerCase(),
|
|
174
|
+
code: code.trim(),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await signInWithCustomToken(getAuth(), result.data.customToken);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The callable names and payloads must match the deployed backend functions.
|
|
181
|
+
The package's `/shared` entry can still be used separately for shared
|
|
182
|
+
constants and validation, but it is not required by the client.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Installation
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
npm install @guettonenrique/passwordless-auth
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Because `/functions` and `/react-native` target separate environments with
|
|
193
|
+
disjoint dependency trees, runtime SDKs are **not declared as top-level
|
|
194
|
+
`dependencies` or `peerDependencies`** (avoiding npm auto-installing the React
|
|
195
|
+
Native stack into a Cloud Functions project or vice versa). Install only what
|
|
196
|
+
your entry needs (exact tested versions live in this package's
|
|
197
|
+
`devDependencies`):
|
|
198
|
+
|
|
199
|
+
| Entry | Needs installed in the consuming project |
|
|
200
|
+
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
201
|
+
| `/functions` | `firebase-functions >=4`, `firebase-admin >=11`, `@sendgrid/mail >=8` (or pass a custom sender) |
|
|
202
|
+
| `/react-native` | `@react-native-firebase/app + auth + functions >=17`, `react >=18`, `react-native >=0.73`, `react-native-keyboard-controller >=1`, `lucide-react-native >=0.300`; optional: `expo-haptics` (haptic feedback) |
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Backend setup (Firebase Cloud Functions, TypeScript)
|
|
207
|
+
|
|
208
|
+
### 1. Register the three functions
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
// functions/src/index.ts
|
|
212
|
+
import { defineSecret } from "firebase-functions/params";
|
|
213
|
+
import {
|
|
214
|
+
createRequestEmailOtp,
|
|
215
|
+
createVerifyEmailOtp,
|
|
216
|
+
createCleanupExpiredOtps,
|
|
217
|
+
} from "@guettonenrique/passwordless-auth/functions";
|
|
218
|
+
import { resolveOrCreateUser } from "./utils/yourUserProvisioning";
|
|
219
|
+
|
|
220
|
+
const sendgridKey = defineSecret("SENDGRID_API_KEY");
|
|
221
|
+
|
|
222
|
+
// The one project-specific hook: resolve or create the user, return the uid.
|
|
223
|
+
const provision = async (email: string): Promise<string> => {
|
|
224
|
+
const { user } = await resolveOrCreateUser(email, { emailVerified: true });
|
|
225
|
+
return user.uid;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
exports.requestEmailOtp = createRequestEmailOtp({
|
|
229
|
+
appName: "MyApp",
|
|
230
|
+
// ⚠️ register the secret on the function so the runtime can resolve it:
|
|
231
|
+
// pass `secrets: [sendgridKey]` via onCall options if your deployment
|
|
232
|
+
// requires it — see "Secrets" below.
|
|
233
|
+
sendgrid: {
|
|
234
|
+
apiKey: sendgridKey, // a params Secret works directly
|
|
235
|
+
fromEmail: "no-reply@yourdomain.com",
|
|
236
|
+
fromName: "MyApp Support",
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
exports.verifyEmailOtp = createVerifyEmailOtp({ provision });
|
|
241
|
+
|
|
242
|
+
// Reaps expired docs (orphans from users who never entered the code).
|
|
243
|
+
exports.cleanupExpiredOtps = createCleanupExpiredOtps();
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`apiKey` accepts a `defineSecret(...)` object directly — the factory resolves
|
|
247
|
+
it per call, which is what makes Firebase secrets work.
|
|
248
|
+
|
|
249
|
+
**Secrets:** declare `SENDGRID_API_KEY` in your `firebase.json` (or use
|
|
250
|
+
`firebase functions:secrets:set SENDGRID_API_KEY`) and reference it with
|
|
251
|
+
`defineSecret`. The factory reads it through the params API at request time.
|
|
252
|
+
|
|
253
|
+
### 2. Lock the OTP collection in Firestore rules
|
|
254
|
+
|
|
255
|
+
```javascript
|
|
256
|
+
match /auth_otps/{document=**} {
|
|
257
|
+
allow read, write: if false; // only the Cloud Functions (admin SDK) may touch it
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### 3. Deploy
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
firebase deploy --only functions,firestore:rules
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Options (all optional, sane defaults)
|
|
268
|
+
|
|
269
|
+
| Option | Default | Notes |
|
|
270
|
+
| -------------------- | -------------------- | --------------------------------------------------------------------------- |
|
|
271
|
+
| `collection` | `'auth_otps'` | same value must be used on all three factories |
|
|
272
|
+
| `cooldownMs` | `60_000` | resend rate limit |
|
|
273
|
+
| `expiryMs` | `600_000` | code lifetime (10 min) |
|
|
274
|
+
| `maxAttempts` | `5` | brute-force cap per code |
|
|
275
|
+
| `enforceAppCheck` | `false` | flip to `true` in production (see below) |
|
|
276
|
+
| `sendEmail` | SendGrid sender | custom `(email: OtpEmail) => Promise<void>` to swap providers or log in dev |
|
|
277
|
+
| `schedule` (cleanup) | `'every 15 minutes'` | Cloud Scheduler syntax |
|
|
278
|
+
|
|
279
|
+
**App Check:** start with `enforceAppCheck: false` while wiring up, then enable
|
|
280
|
+
it in production once your app ships App Check (React Native: `@react-native-firebase/app-check`).
|
|
281
|
+
|
|
282
|
+
**Dev mode without SendGrid:**
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
exports.requestEmailOtp = createRequestEmailOtp({
|
|
286
|
+
appName: "MyApp",
|
|
287
|
+
sendEmail: async (email) => {
|
|
288
|
+
console.log(`🔑 [DEV OTP] ${email.to}: ${email.code}`);
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Error contract (surfaced verbatim to users)
|
|
294
|
+
|
|
295
|
+
| Code | Meaning |
|
|
296
|
+
| -------------------- | --------------------------------------------------------------------------------- |
|
|
297
|
+
| `invalid-argument` | Bad email, wrong code format, or wrong code (message includes remaining attempts) |
|
|
298
|
+
| `not-found` | No active code for this email |
|
|
299
|
+
| `deadline-exceeded` | Code expired |
|
|
300
|
+
| `resource-exhausted` | Cooldown active or attempts exhausted |
|
|
301
|
+
| `internal` | Email delivery failed (doc is auto-cleaned) |
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## Mobile setup (React Native / Expo)
|
|
306
|
+
|
|
307
|
+
### 1. Add to your auth screen
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
import { useState } from 'react';
|
|
311
|
+
import { EmailOtpModal } from '@guettonenrique/passwordless-auth/react-native';
|
|
312
|
+
|
|
313
|
+
const [otpVisible, setOtpVisible] = useState(false);
|
|
314
|
+
|
|
315
|
+
<TouchableOpacity onPress={() => setOtpVisible(true)}>
|
|
316
|
+
<Text>Continue with email code</Text>
|
|
317
|
+
</TouchableOpacity>
|
|
318
|
+
|
|
319
|
+
<EmailOtpModal
|
|
320
|
+
visible={otpVisible}
|
|
321
|
+
onClose={() => setOtpVisible(false)}
|
|
322
|
+
initialEmail={email} // optional pre-fill
|
|
323
|
+
/>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
That's the whole integration. On success the modal closes and your existing
|
|
327
|
+
`onAuthStateChanged` listener flips the app into the signed-in state — no
|
|
328
|
+
navigation code needed.
|
|
329
|
+
|
|
330
|
+
### 2. Or use the pieces yourself
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
import {
|
|
334
|
+
requestEmailOtp, // (email) → { status, message }
|
|
335
|
+
verifyEmailOtp, // (email, code) → customToken
|
|
336
|
+
signInWithEmailOtp, // (email, code) → signs in via signInWithCustomToken
|
|
337
|
+
OtpInput, // 6-slot auto-fill code input
|
|
338
|
+
} from "@guettonenrique/passwordless-auth/react-native";
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
`OtpInput` uses one hidden native input (the only way paste + iOS `oneTimeCode`
|
|
342
|
+
/ Android `one-time-code` auto-fill work reliably) and calls `onComplete` when
|
|
343
|
+
the last digit arrives — wire it to `signInWithEmailOtp` for auto-submit.
|
|
344
|
+
|
|
345
|
+
### 3. Keep the UI in sync with the backend
|
|
346
|
+
|
|
347
|
+
If you override `cooldownMs`/`expiryMs` on the backend, pass the matching
|
|
348
|
+
`cooldownSeconds` prop to `EmailOtpModal` so the UI countdown mirrors the
|
|
349
|
+
server rate limit.
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## Testing the package (contributors)
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
npm install
|
|
357
|
+
npm run typecheck # 3 per-entry tsc passes
|
|
358
|
+
npm run lint # includes the cross-entry boundary rule
|
|
359
|
+
npm test # 58 tests: shared, functions (mocked Firestore/Auth), react-native
|
|
360
|
+
npm run build # tsup: ESM+CJS+DTS for each entry
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**Boundary rule:** `src/shared` may import nothing; `src/functions` may not
|
|
364
|
+
import `src/react-native` and vice versa. ESLint enforces it on import
|
|
365
|
+
specifiers; per-entry `tsc` configs keep the type graphs separate.
|
|
366
|
+
|
|
367
|
+
**Publishing** (manual for v0.x; CI-on-tag from 1.0):
|
|
368
|
+
|
|
369
|
+
The package is published publicly to **npmjs** (`publishConfig` sets
|
|
370
|
+
`"registry": "https://registry.npmjs.org"` and `"access": "public"`):
|
|
371
|
+
|
|
372
|
+
1. Authenticate with npm:
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
npm login
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
2. Publish (`prepublishOnly` runs `npm run build` automatically):
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
npm publish
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
3. Verify:
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
npm view @guettonenrique/passwordless-auth
|
|
388
|
+
```
|