@proveanything/smartlinks 2.0.69 → 2.0.70
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/api/authKit.d.ts +28 -4
- package/dist/api/authKit.js +35 -1
- package/dist/docs/API_SUMMARY.md +73 -11
- package/dist/docs/auth-kit.md +46 -5
- package/dist/docs/members.md +182 -0
- package/dist/docs/overview.md +476 -475
- package/dist/docs/server-functions.md +4 -1
- package/dist/openapi.yaml +201 -20
- package/dist/testing/index.js +1 -1
- package/dist/types/appManifest.d.ts +16 -4
- package/dist/types/authKit.d.ts +58 -7
- package/docs/API_SUMMARY.md +73 -11
- package/docs/auth-kit.md +46 -5
- package/docs/members.md +182 -0
- package/docs/overview.md +476 -475
- package/docs/server-functions.md +4 -1
- package/openapi.yaml +201 -20
- package/package.json +1 -1
package/dist/api/authKit.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AuthLoginResponse, AppleLoginOptions, RefreshResponse, LogoutResponse, PhoneSendCodeResponse, PhoneVerifyResponse, PasswordResetRequestResponse, VerifyResetTokenResponse, PasswordResetCompleteResponse, EmailVerificationActionResponse, EmailVerifyTokenResponse, AuthKitConfig, AuthKitConfigInput, AuthKitSetIdentityResponse, MagicLinkSendResponse, MagicLinkVerifyResponse, UserProfile, UpdateProfileResponse, ProfileUpdateData, SuccessResponse, SendWhatsAppRequest, SendWhatsAppResponse, ExchangeWhatsAppSessionResponse, VerifyWhatsAppResponse, WhatsAppStatusResponse, SendSmsVerifyRequest, SendSmsVerifyResponse, VerifySmsResponse, UpsertContactRequest, UpsertContactResponse, MfaChallengeSendResponse, MfaFinalizeResponse, MfaEnrollSendResponse, MfaEnrolledResponse, MfaFactorsResponse, TrustedDevice } from "../types/authKit.js";
|
|
1
|
+
import type { AuthLoginResponse, AppleLoginOptions, RefreshResponse, LogoutResponse, PhoneSendCodeResponse, PhoneVerifyResponse, PasswordResetRequestResponse, VerifyResetTokenResponse, PasswordResetCompleteResponse, EmailVerificationActionResponse, EmailVerifyTokenResponse, AuthKitConfig, AuthKitConfigInput, AuthKitSetIdentityResponse, AuthKitInviteRequest, AuthKitInviteResponse, MagicLinkSendResponse, MagicLinkVerifyResponse, UserProfile, UpdateProfileResponse, ProfileUpdateData, SuccessResponse, SendWhatsAppRequest, SendWhatsAppResponse, ExchangeWhatsAppSessionResponse, VerifyWhatsAppResponse, WhatsAppStatusResponse, SendSmsVerifyRequest, SendSmsVerifyResponse, VerifySmsResponse, UpsertContactRequest, UpsertContactResponse, MfaChallengeSendResponse, MfaFinalizeResponse, MfaEnrollSendResponse, MfaTotpEnrollResponse, MfaEnrolledResponse, MfaFactor, MfaFactorsResponse, TrustedDevice } from "../types/authKit.js";
|
|
2
2
|
/**
|
|
3
3
|
* Namespace containing helper functions for the new AuthKit API.
|
|
4
4
|
* Legacy collection-based authKit helpers retained (marked as *Legacy*).
|
|
@@ -208,8 +208,11 @@ export declare namespace authKit {
|
|
|
208
208
|
function verifyEmailChange(clientId: string, token: string): Promise<SuccessResponse>;
|
|
209
209
|
function updatePhone(clientId: string, phoneNumber: string, verificationCode: string): Promise<SuccessResponse>;
|
|
210
210
|
function deleteAccount(clientId: string, password: string, confirmText: string): Promise<SuccessResponse>;
|
|
211
|
-
/**
|
|
212
|
-
|
|
211
|
+
/**
|
|
212
|
+
* Send (or resend) an MFA challenge code to the given factor (public). For `totp` nothing is sent —
|
|
213
|
+
* it tells the server the code will come from their authenticator app; then {@link mfaChallengeVerify}.
|
|
214
|
+
*/
|
|
215
|
+
function mfaChallengeSend(clientId: string, mfaSessionToken: string, factor: MfaFactor): Promise<MfaChallengeSendResponse>;
|
|
213
216
|
/**
|
|
214
217
|
* Verify an MFA challenge code and finalize the login (public). On success this behaves
|
|
215
218
|
* like {@link login} — the bearer token is adopted and the cache invalidated.
|
|
@@ -233,6 +236,15 @@ export declare namespace authKit {
|
|
|
233
236
|
function enrollSmsMfa(clientId: string, phoneNumber: string): Promise<MfaEnrollSendResponse>;
|
|
234
237
|
/** Confirm SMS-factor enrollment with the code sent by {@link enrollSmsMfa} (authenticated). */
|
|
235
238
|
function confirmSmsMfa(clientId: string, mfaSessionToken: string, code: string): Promise<MfaEnrolledResponse>;
|
|
239
|
+
/**
|
|
240
|
+
* Begin authenticator-app enrollment (authenticated): a new secret for Google Authenticator,
|
|
241
|
+
* Microsoft Authenticator, 1Password and the like. Show `otpauthUri` as a QR code (and `secret` for
|
|
242
|
+
* typing in), then {@link confirmTotpMfa} with the code the app shows. Nothing is kept until then;
|
|
243
|
+
* setting up again replaces the previous app once confirmed.
|
|
244
|
+
*/
|
|
245
|
+
function enrollTotpMfa(clientId: string): Promise<MfaTotpEnrollResponse>;
|
|
246
|
+
/** Confirm authenticator-app enrollment with the 6-digit code the app shows (authenticated). */
|
|
247
|
+
function confirmTotpMfa(clientId: string, mfaSessionToken: string, code: string): Promise<MfaEnrolledResponse>;
|
|
236
248
|
/**
|
|
237
249
|
* Generate a fresh set of recovery codes, invalidating any previous set (authenticated).
|
|
238
250
|
* Returned **in plaintext, exactly once** — nothing else in the API ever returns them
|
|
@@ -242,7 +254,7 @@ export declare namespace authKit {
|
|
|
242
254
|
recoveryCodes: string[];
|
|
243
255
|
}>;
|
|
244
256
|
/** Remove an enrolled MFA factor (authenticated). Server route is DELETE with a body. */
|
|
245
|
-
function removeMfaFactor(clientId: string, factor:
|
|
257
|
+
function removeMfaFactor(clientId: string, factor: MfaFactor, password: string): Promise<SuccessResponse>;
|
|
246
258
|
/** List devices trusted to skip MFA challenges for the current user (authenticated). */
|
|
247
259
|
function listTrustedDevices(clientId: string): Promise<{
|
|
248
260
|
devices: TrustedDevice[];
|
|
@@ -281,5 +293,17 @@ export declare namespace authKit {
|
|
|
281
293
|
function setIdentity(collectionId: string, authKitId: string, body: {
|
|
282
294
|
mode: 'own';
|
|
283
295
|
}): Promise<AuthKitSetIdentityResponse>;
|
|
296
|
+
/**
|
|
297
|
+
* Invite someone to be a member of a site with its own logins (admin). Makes their account if they have
|
|
298
|
+
* none (the only way in when the site is invitation only — `signUp: 'invite'`), finds or creates their
|
|
299
|
+
* contact in this collection with the `customFields` you pass (their level), links the two, and emails
|
|
300
|
+
* them a sign-in link to the site (`redirectUrl`, default the kit's) that works once for 7 days. The page
|
|
301
|
+
* it opens handles it like any magic link: `mode=magicLink&token=…` → {@link verifyMagicLink}.
|
|
302
|
+
* Inviting someone again sends a fresh link.
|
|
303
|
+
*
|
|
304
|
+
* Errors: `OWN_LOGINS_REQUIRED` (409), `INVALID_EMAIL` (400), `INVALID_REDIRECT_URL` (400, not one of
|
|
305
|
+
* this site's addresses), `TOO_MANY_INVITES` (429, 5 an hour per address).
|
|
306
|
+
*/
|
|
307
|
+
function invite(collectionId: string, authKitId: string, body: AuthKitInviteRequest): Promise<AuthKitInviteResponse>;
|
|
284
308
|
function remove(collectionId: string, authKitId: string): Promise<void>;
|
|
285
309
|
}
|
package/dist/api/authKit.js
CHANGED
|
@@ -380,7 +380,10 @@ export var authKit;
|
|
|
380
380
|
* challenge/send can be called again on the same token (while unexpired/unburned) to
|
|
381
381
|
* switch factors or resend.
|
|
382
382
|
* =================================== */
|
|
383
|
-
/**
|
|
383
|
+
/**
|
|
384
|
+
* Send (or resend) an MFA challenge code to the given factor (public). For `totp` nothing is sent —
|
|
385
|
+
* it tells the server the code will come from their authenticator app; then {@link mfaChallengeVerify}.
|
|
386
|
+
*/
|
|
384
387
|
async function mfaChallengeSend(clientId, mfaSessionToken, factor) {
|
|
385
388
|
return post(`/authkit/${encodeURIComponent(clientId)}/mfa/challenge/send`, { mfaSessionToken, factor });
|
|
386
389
|
}
|
|
@@ -442,6 +445,21 @@ export var authKit;
|
|
|
442
445
|
return post(`/authkit/${encodeURIComponent(clientId)}/mfa/factors/sms/confirm`, { mfaSessionToken, code });
|
|
443
446
|
}
|
|
444
447
|
authKit.confirmSmsMfa = confirmSmsMfa;
|
|
448
|
+
/**
|
|
449
|
+
* Begin authenticator-app enrollment (authenticated): a new secret for Google Authenticator,
|
|
450
|
+
* Microsoft Authenticator, 1Password and the like. Show `otpauthUri` as a QR code (and `secret` for
|
|
451
|
+
* typing in), then {@link confirmTotpMfa} with the code the app shows. Nothing is kept until then;
|
|
452
|
+
* setting up again replaces the previous app once confirmed.
|
|
453
|
+
*/
|
|
454
|
+
async function enrollTotpMfa(clientId) {
|
|
455
|
+
return post(`/authkit/${encodeURIComponent(clientId)}/mfa/factors/totp/enroll`, {});
|
|
456
|
+
}
|
|
457
|
+
authKit.enrollTotpMfa = enrollTotpMfa;
|
|
458
|
+
/** Confirm authenticator-app enrollment with the 6-digit code the app shows (authenticated). */
|
|
459
|
+
async function confirmTotpMfa(clientId, mfaSessionToken, code) {
|
|
460
|
+
return post(`/authkit/${encodeURIComponent(clientId)}/mfa/factors/totp/confirm`, { mfaSessionToken, code });
|
|
461
|
+
}
|
|
462
|
+
authKit.confirmTotpMfa = confirmTotpMfa;
|
|
445
463
|
/**
|
|
446
464
|
* Generate a fresh set of recovery codes, invalidating any previous set (authenticated).
|
|
447
465
|
* Returned **in plaintext, exactly once** — nothing else in the API ever returns them
|
|
@@ -525,6 +543,22 @@ export var authKit;
|
|
|
525
543
|
return post(path, body);
|
|
526
544
|
}
|
|
527
545
|
authKit.setIdentity = setIdentity;
|
|
546
|
+
/**
|
|
547
|
+
* Invite someone to be a member of a site with its own logins (admin). Makes their account if they have
|
|
548
|
+
* none (the only way in when the site is invitation only — `signUp: 'invite'`), finds or creates their
|
|
549
|
+
* contact in this collection with the `customFields` you pass (their level), links the two, and emails
|
|
550
|
+
* them a sign-in link to the site (`redirectUrl`, default the kit's) that works once for 7 days. The page
|
|
551
|
+
* it opens handles it like any magic link: `mode=magicLink&token=…` → {@link verifyMagicLink}.
|
|
552
|
+
* Inviting someone again sends a fresh link.
|
|
553
|
+
*
|
|
554
|
+
* Errors: `OWN_LOGINS_REQUIRED` (409), `INVALID_EMAIL` (400), `INVALID_REDIRECT_URL` (400, not one of
|
|
555
|
+
* this site's addresses), `TOO_MANY_INVITES` (429, 5 an hour per address).
|
|
556
|
+
*/
|
|
557
|
+
async function invite(collectionId, authKitId, body) {
|
|
558
|
+
const path = `/admin/collection/${encodeURIComponent(collectionId)}/authKit/${encodeURIComponent(authKitId)}/invite`;
|
|
559
|
+
return post(path, body);
|
|
560
|
+
}
|
|
561
|
+
authKit.invite = invite;
|
|
528
562
|
async function remove(collectionId, authKitId) {
|
|
529
563
|
const path = `/admin/collection/${encodeURIComponent(collectionId)}/authKit/${encodeURIComponent(authKitId)}`;
|
|
530
564
|
return del(path);
|
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.70 | Generated: 2026-10-24T08:16:51.896Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -2808,7 +2808,7 @@ interface ServerFunctionSl {
|
|
|
2808
2808
|
tags: { resolve(id: string): Promise<ResolvedTag | null> };
|
|
2809
2809
|
contacts: {
|
|
2810
2810
|
upsert(details: ContactUpsertDetails): Promise<{ contactId: string; created: boolean }>;
|
|
2811
|
-
forCaller(): Promise<
|
|
2811
|
+
forCaller(): Promise<CallerContact | null>;
|
|
2812
2812
|
};
|
|
2813
2813
|
interactions: { record(event: ServerFunctionInteractionEvent): Promise<{ eventId: string }> };
|
|
2814
2814
|
tools: { run<T = any>(name: string, args?: Record<string, any>): Promise<T> };
|
|
@@ -2830,6 +2830,20 @@ interface ServerFunctionCaller {
|
|
|
2830
2830
|
}
|
|
2831
2831
|
```
|
|
2832
2832
|
|
|
2833
|
+
**CallerContact** (interface)
|
|
2834
|
+
```typescript
|
|
2835
|
+
interface CallerContact {
|
|
2836
|
+
contactId: string;
|
|
2837
|
+
email: string | null;
|
|
2838
|
+
phone: string | null;
|
|
2839
|
+
firstName: string | null;
|
|
2840
|
+
lastName: string | null;
|
|
2841
|
+
displayName: string | null;
|
|
2842
|
+
tags: string[];
|
|
2843
|
+
customFields: Record<string, unknown>;
|
|
2844
|
+
}
|
|
2845
|
+
```
|
|
2846
|
+
|
|
2833
2847
|
**ServerFunctionContext** (interface)
|
|
2834
2848
|
```typescript
|
|
2835
2849
|
interface ServerFunctionContext {
|
|
@@ -4565,9 +4579,9 @@ interface AppleLoginOptions {
|
|
|
4565
4579
|
```typescript
|
|
4566
4580
|
interface MfaRequiredDetails {
|
|
4567
4581
|
mfaSessionToken: string
|
|
4568
|
-
availableFactors:
|
|
4569
|
-
preferredFactor:
|
|
4570
|
-
maskedDestinations: { email?: string; sms?: string }
|
|
4582
|
+
availableFactors: MfaFactor[]
|
|
4583
|
+
preferredFactor: MfaFactor
|
|
4584
|
+
maskedDestinations: { email?: string; sms?: string; totp?: string }
|
|
4571
4585
|
}
|
|
4572
4586
|
```
|
|
4573
4587
|
|
|
@@ -4575,7 +4589,7 @@ interface MfaRequiredDetails {
|
|
|
4575
4589
|
```typescript
|
|
4576
4590
|
interface MfaChallengeSendResponse {
|
|
4577
4591
|
success: true
|
|
4578
|
-
factor:
|
|
4592
|
+
factor: MfaFactor
|
|
4579
4593
|
destination: string
|
|
4580
4594
|
expiresAt: string
|
|
4581
4595
|
}
|
|
@@ -4590,11 +4604,23 @@ interface MfaEnrollSendResponse {
|
|
|
4590
4604
|
}
|
|
4591
4605
|
```
|
|
4592
4606
|
|
|
4607
|
+
**MfaTotpEnrollResponse** (interface)
|
|
4608
|
+
```typescript
|
|
4609
|
+
interface MfaTotpEnrollResponse {
|
|
4610
|
+
mfaSessionToken: string
|
|
4611
|
+
secret: string
|
|
4612
|
+
otpauthUri: string
|
|
4613
|
+
issuer: string
|
|
4614
|
+
account: string
|
|
4615
|
+
expiresAt: string
|
|
4616
|
+
}
|
|
4617
|
+
```
|
|
4618
|
+
|
|
4593
4619
|
**MfaEnrolledResponse** (interface)
|
|
4594
4620
|
```typescript
|
|
4595
4621
|
interface MfaEnrolledResponse {
|
|
4596
4622
|
enrolled: true
|
|
4597
|
-
factor:
|
|
4623
|
+
factor: MfaFactor
|
|
4598
4624
|
}
|
|
4599
4625
|
```
|
|
4600
4626
|
|
|
@@ -4604,6 +4630,7 @@ interface MfaFactorsResponse {
|
|
|
4604
4630
|
enrolledFactors: {
|
|
4605
4631
|
email?: { enrolledAt: string; destination: string }
|
|
4606
4632
|
sms?: { enrolledAt: string; destination: string }
|
|
4633
|
+
totp?: { enrolledAt: string; destination: string }
|
|
4607
4634
|
}
|
|
4608
4635
|
recoveryCodesRemaining: number
|
|
4609
4636
|
mfaEnabledForClient: boolean
|
|
@@ -4866,6 +4893,30 @@ interface AuthKitConfig {
|
|
|
4866
4893
|
security?: AuthKitSecurityConfig
|
|
4867
4894
|
collectionIds?: string[]
|
|
4868
4895
|
identity?: AuthKitIdentity
|
|
4896
|
+
signUp?: 'open' | 'invite'
|
|
4897
|
+
}
|
|
4898
|
+
```
|
|
4899
|
+
|
|
4900
|
+
**AuthKitInviteRequest** (interface)
|
|
4901
|
+
```typescript
|
|
4902
|
+
interface AuthKitInviteRequest {
|
|
4903
|
+
email: string
|
|
4904
|
+
firstName?: string
|
|
4905
|
+
lastName?: string
|
|
4906
|
+
displayName?: string
|
|
4907
|
+
customFields?: Record<string, unknown>
|
|
4908
|
+
redirectUrl?: string
|
|
4909
|
+
}
|
|
4910
|
+
```
|
|
4911
|
+
|
|
4912
|
+
**AuthKitInviteResponse** (interface)
|
|
4913
|
+
```typescript
|
|
4914
|
+
interface AuthKitInviteResponse {
|
|
4915
|
+
ok: true
|
|
4916
|
+
userId: string
|
|
4917
|
+
contactId: string
|
|
4918
|
+
created: boolean
|
|
4919
|
+
expiresAt: string
|
|
4869
4920
|
}
|
|
4870
4921
|
```
|
|
4871
4922
|
|
|
@@ -4934,6 +4985,8 @@ interface AuthKitLockoutPolicy {
|
|
|
4934
4985
|
|
|
4935
4986
|
**AuthKitErrorCode** = ``
|
|
4936
4987
|
|
|
4988
|
+
**MfaFactor** = `'email' | 'sms' | 'totp'`
|
|
4989
|
+
|
|
4937
4990
|
**MfaErrorCode** = ``
|
|
4938
4991
|
|
|
4939
4992
|
**VerifyStatus** = `'pending' | 'verified' | 'failed' | 'expired' | 'unknown'`
|
|
@@ -11291,8 +11344,8 @@ Update the authenticated user's profile and replace the bearer token when refres
|
|
|
11291
11344
|
**deleteAccount**(clientId: string, password: string, confirmText: string) → `Promise<SuccessResponse>`
|
|
11292
11345
|
Update the authenticated user's profile and replace the bearer token when refreshed claims are returned.
|
|
11293
11346
|
|
|
11294
|
-
**mfaChallengeSend**(clientId: string, mfaSessionToken: string, factor:
|
|
11295
|
-
Send (or resend) an MFA challenge code to the given factor (public).
|
|
11347
|
+
**mfaChallengeSend**(clientId: string, mfaSessionToken: string, factor: MfaFactor) → `Promise<MfaChallengeSendResponse>`
|
|
11348
|
+
Send (or resend) an MFA challenge code to the given factor (public). For `totp` nothing is sent — it tells the server the code will come from their authenticator app; then {@link mfaChallengeVerify}.
|
|
11296
11349
|
|
|
11297
11350
|
**mfaChallengeVerify**(clientId: string, mfaSessionToken: string, code: string, trustDevice?: boolean, deviceLabel?: string) → `Promise<MfaFinalizeResponse>`
|
|
11298
11351
|
Verify an MFA challenge code and finalize the login (public). On success this behaves like {@link login} — the bearer token is adopted and the cache invalidated. it and pass it to future {@link login} calls to skip the challenge on this device.
|
|
@@ -11315,10 +11368,16 @@ Begin SMS-factor enrollment; sends a code to the given phone number (authenticat
|
|
|
11315
11368
|
**confirmSmsMfa**(clientId: string, mfaSessionToken: string, code: string) → `Promise<MfaEnrolledResponse>`
|
|
11316
11369
|
Confirm SMS-factor enrollment with the code sent by {@link enrollSmsMfa} (authenticated).
|
|
11317
11370
|
|
|
11371
|
+
**enrollTotpMfa**(clientId: string) → `Promise<MfaTotpEnrollResponse>`
|
|
11372
|
+
Begin authenticator-app enrollment (authenticated): a new secret for Google Authenticator, Microsoft Authenticator, 1Password and the like. Show `otpauthUri` as a QR code (and `secret` for typing in), then {@link confirmTotpMfa} with the code the app shows. Nothing is kept until then; setting up again replaces the previous app once confirmed.
|
|
11373
|
+
|
|
11374
|
+
**confirmTotpMfa**(clientId: string, mfaSessionToken: string, code: string) → `Promise<MfaEnrolledResponse>`
|
|
11375
|
+
Confirm authenticator-app enrollment with the 6-digit code the app shows (authenticated).
|
|
11376
|
+
|
|
11318
11377
|
**generateMfaRecoveryCodes**(clientId: string, password: string) → `Promise<`
|
|
11319
11378
|
Generate a fresh set of recovery codes, invalidating any previous set (authenticated). Returned **in plaintext, exactly once** — nothing else in the API ever returns them again, so the caller must display/export them immediately.
|
|
11320
11379
|
|
|
11321
|
-
**removeMfaFactor**(clientId: string, factor:
|
|
11380
|
+
**removeMfaFactor**(clientId: string, factor: MfaFactor, password: string) → `Promise<SuccessResponse>`
|
|
11322
11381
|
Remove an enrolled MFA factor (authenticated). Server route is DELETE with a body.
|
|
11323
11382
|
|
|
11324
11383
|
**listTrustedDevices**(clientId: string) → `Promise<`
|
|
@@ -11345,8 +11404,11 @@ Update an AuthKit client config (admin). This is how the account **security poli
|
|
|
11345
11404
|
**setIdentity**(collectionId: string, authKitId: string, body: { mode: 'own' }) → `Promise<AuthKitSetIdentityResponse>`
|
|
11346
11405
|
Give this AuthKit its **own logins** (admin): its members become accounts of this site only — the same email can be a separate account, with its own password, on another site — rather than SmartLinks accounts shared across sites. One way: a site with its own members can't go back to the shared pool. Members who signed up before keep their SmartLinks account until moved across. Errors: `MEMBER_POOL_CONFLICT` (409) when another AuthKit already gives one of its collections its own logins (one per collection); `PERMISSION_DENIED` (403) unless you administer every collection it serves.
|
|
11347
11406
|
|
|
11407
|
+
**invite**(collectionId: string, authKitId: string, body: AuthKitInviteRequest) → `Promise<AuthKitInviteResponse>`
|
|
11408
|
+
Invite someone to be a member of a site with its own logins (admin). Makes their account if they have none (the only way in when the site is invitation only — `signUp: 'invite'`), finds or creates their contact in this collection with the `customFields` you pass (their level), links the two, and emails them a sign-in link to the site (`redirectUrl`, default the kit's) that works once for 7 days. The page it opens handles it like any magic link: `mode=magicLink&token=…` → {@link verifyMagicLink}. Inviting someone again sends a fresh link. Errors: `OWN_LOGINS_REQUIRED` (409), `INVALID_EMAIL` (400), `INVALID_REDIRECT_URL` (400, not one of this site's addresses), `TOO_MANY_INVITES` (429, 5 an hour per address).
|
|
11409
|
+
|
|
11348
11410
|
**remove**(collectionId: string, authKitId: string) → `Promise<void>`
|
|
11349
|
-
|
|
11411
|
+
Invite someone to be a member of a site with its own logins (admin). Makes their account if they have none (the only way in when the site is invitation only — `signUp: 'invite'`), finds or creates their contact in this collection with the `customFields` you pass (their level), links the two, and emails them a sign-in link to the site (`redirectUrl`, default the kit's) that works once for 7 days. The page it opens handles it like any magic link: `mode=magicLink&token=…` → {@link verifyMagicLink}. Inviting someone again sends a fresh link. Errors: `OWN_LOGINS_REQUIRED` (409), `INVALID_EMAIL` (400), `INVALID_REDIRECT_URL` (400, not one of this site's addresses), `TOO_MANY_INVITES` (429, 5 an hour per address).
|
|
11350
11412
|
|
|
11351
11413
|
### batch
|
|
11352
11414
|
|
package/dist/docs/auth-kit.md
CHANGED
|
@@ -305,11 +305,16 @@ clearPersistedTokens();
|
|
|
305
305
|
|
|
306
306
|
---
|
|
307
307
|
|
|
308
|
-
## Step-up MFA
|
|
308
|
+
## Step-up MFA
|
|
309
309
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
310
|
+
Factors: **email code, SMS code, authenticator app (`totp`), recovery codes**. The shared login UI
|
|
311
|
+
(`@proveanything/smartlinks-auth-ui`) has the challenge and the Settings → Security screens. WhatsApp
|
|
312
|
+
codes and passkeys are planned; don't build against them yet.
|
|
313
|
+
|
|
314
|
+
**Authenticator apps** (Google Authenticator, Microsoft Authenticator, 1Password, Authy…) are the
|
|
315
|
+
recommended second step: nothing is sent (no SMS cost, nothing to intercept), and they are offered first
|
|
316
|
+
when someone has one. A client can switch any factor off with `mfa.factorsEnabled` (e.g.
|
|
317
|
+
`{ totp: false }`).
|
|
313
318
|
|
|
314
319
|
The step-up gate applies to **every login method that issues a session**:
|
|
315
320
|
|
|
@@ -408,15 +413,25 @@ await authKit.confirmEmailMfa(clientId, mfaSessionToken, code);
|
|
|
408
413
|
const { mfaSessionToken: smsToken } = await authKit.enrollSmsMfa(clientId, '+61400000000');
|
|
409
414
|
await authKit.confirmSmsMfa(clientId, smsToken, code);
|
|
410
415
|
|
|
416
|
+
// Enroll an authenticator app: show otpauthUri as a QR code (and secret for typing in),
|
|
417
|
+
// then confirm with the 6-digit code the app shows
|
|
418
|
+
const { mfaSessionToken: appToken, otpauthUri, secret } = await authKit.enrollTotpMfa(clientId);
|
|
419
|
+
await authKit.confirmTotpMfa(clientId, appToken, codeFromApp);
|
|
420
|
+
|
|
411
421
|
// Recovery codes — plaintext, shown exactly once; nothing else in the API returns them again
|
|
412
422
|
const { recoveryCodes } = await authKit.generateMfaRecoveryCodes(clientId, password);
|
|
413
423
|
|
|
414
424
|
// Inspect / remove
|
|
415
425
|
const factors = await authKit.getMfaFactors(clientId);
|
|
416
|
-
// factors: { enrolledFactors: { email?, sms? }, recoveryCodesRemaining, mfaEnabledForClient }
|
|
426
|
+
// factors: { enrolledFactors: { email?, sms?, totp? }, recoveryCodesRemaining, mfaEnabledForClient }
|
|
417
427
|
await authKit.removeMfaFactor(clientId, 'sms', password);
|
|
418
428
|
```
|
|
419
429
|
|
|
430
|
+
At sign-in, `MFA_REQUIRED` lists `availableFactors` with `preferredFactor` (`totp`, then `sms`, then
|
|
431
|
+
`email`). For `totp`, `mfaChallengeSend(clientId, token, 'totp')` sends nothing; it just says the code
|
|
432
|
+
comes from the app. Then call `mfaChallengeVerify` with that code, as for the others. Each authenticator
|
|
433
|
+
code works once. The secret is sealed on the server and never returned after `enrollTotpMfa`.
|
|
434
|
+
|
|
420
435
|
> Enrollment endpoints don't currently check the client's `mfa.mode` — enrolling is possible
|
|
421
436
|
> even when MFA is configured `'off'` for a client. Those factors simply won't be challenged
|
|
422
437
|
> at login until an admin turns the mode on (via the existing admin AuthKit config update,
|
|
@@ -633,6 +648,31 @@ await authKit.setIdentity(collectionId, authKitId, { mode: 'own' })
|
|
|
633
648
|
resets) works the same way; nothing changes in the login UI or your code.
|
|
634
649
|
- `identity` is read-only on `create` / `update`; it is shown on the config (`authKit.get`).
|
|
635
650
|
|
|
651
|
+
### Invitation only
|
|
652
|
+
|
|
653
|
+
A site with its own logins can let in only the people it invites:
|
|
654
|
+
|
|
655
|
+
```ts
|
|
656
|
+
await authKit.update(collectionId, authKitId, { signUp: 'invite' }) // 'open' (default) | 'invite'
|
|
657
|
+
await authKit.invite(collectionId, authKitId, { email: 'jo@example.com', firstName: 'Jo', customFields: { level: 'member' } })
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
- **Enforced on the server, for every route.** Nobody without an account can get one: `register`,
|
|
661
|
+
`verifyMagicLink`, `verifyPhoneCode`, Google, Apple, LinkedIn and WhatsApp answer `SIGN_UP_CLOSED`
|
|
662
|
+
(403). `sendMagicLink` and `sendPhoneCode` answer as if sent, but send nothing to someone who isn't a
|
|
663
|
+
member, so nobody can find out who is.
|
|
664
|
+
- **`invite`** makes the person's account if they have none, finds or creates their contact in that
|
|
665
|
+
collection with the fields you pass, links the two, and emails a sign-in link to the site (`redirectUrl`,
|
|
666
|
+
default the kit's) that works once for 7 days. It lands like any magic link (`mode=magicLink&token=…`).
|
|
667
|
+
Inviting someone again sends a fresh link. The link is never returned to the admin.
|
|
668
|
+
- **Invited members then sign in any way the site offers.** A magic link, "forgot password" to set a
|
|
669
|
+
password, or Google/Apple/LinkedIn with the same verified email: an invited account nobody has signed
|
|
670
|
+
in to yet can be claimed that way.
|
|
671
|
+
- `signUp` is on the public config (`load`): hide "Create an account" when it's `invite`. Setting it
|
|
672
|
+
needs own logins first (`OWN_LOGINS_REQUIRED`, 409).
|
|
673
|
+
- On any site, a member signing in with a verified email or phone is attached to the contact the
|
|
674
|
+
collection already has for it, so fields the staff set beforehand (a level) are theirs.
|
|
675
|
+
|
|
636
676
|
## Relationship to other parts of the SDK
|
|
637
677
|
|
|
638
678
|
| Concern | Where it lives |
|
|
@@ -646,5 +686,6 @@ await authKit.setIdentity(collectionId, authKitId, { mode: 'own' })
|
|
|
646
686
|
|
|
647
687
|
## Further reading
|
|
648
688
|
|
|
689
|
+
- [members.md](members.md) — members with a level: clubs, leagues, members-only content
|
|
649
690
|
- [app-data-storage.md](app-data-storage.md) — storing user-specific data after login
|
|
650
691
|
- [app-manifest.md](app-manifest.md) — `app.admin.json` setup questions for configuring `clientId`
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Members: signed-in people with a level
|
|
2
|
+
|
|
3
|
+
A club, a league, a wine club, a members' area. SmartLinks has **no separate "membership" object**,
|
|
4
|
+
and you don't need one:
|
|
5
|
+
|
|
6
|
+
| Piece | Where it lives |
|
|
7
|
+
|---|---|
|
|
8
|
+
| A member's login | The **auth kit**: the site's own logins (auth-kit.md → "Own logins") |
|
|
9
|
+
| The member | Their **contact** (one per person, created when they sign in or fill in a form) |
|
|
10
|
+
| What they are (gold, silver, junior, pending) | A **contact custom field**, e.g. `customFields.level` |
|
|
11
|
+
| Who may see or change it | The contact field's **public** setting: hidden, visible or editable |
|
|
12
|
+
| Members-only content | A **server function** that checks the level, then returns the content |
|
|
13
|
+
| Managing members | **Studio → Customers**: find people, set their level, email a segment |
|
|
14
|
+
|
|
15
|
+
**The contract: whoever manages membership writes the member's `level` field.** That can be the owner
|
|
16
|
+
by hand in Studio, a memberships app after a payment, or a connector syncing a club's existing system.
|
|
17
|
+
The site only ever reads the field, so the way people become members can change without rebuilding
|
|
18
|
+
the site.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Questions to settle first
|
|
23
|
+
|
|
24
|
+
Ask these before building, because they change what you build:
|
|
25
|
+
|
|
26
|
+
1. **Who can join?** Anyone; anyone, but the club confirms them; or **only people the club invites**
|
|
27
|
+
(no sign-up at all)?
|
|
28
|
+
2. **Who sets the level?** The owner (people sign up, then the owner confirms them), or the person
|
|
29
|
+
themselves? Let people choose only a level that unlocks nothing, like "junior / adult".
|
|
30
|
+
3. **What does each level get?** Pages, results, discounts, early access, booking.
|
|
31
|
+
4. **Fees?** If the club takes payment by hand, the owner sets the level once they've been paid. Paying
|
|
32
|
+
on the site is a memberships app's job. Never take card details in a site.
|
|
33
|
+
5. **Do members see each other?** A members' directory needs consent: a "show me in the directory"
|
|
34
|
+
field the member sets themselves.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 1. Sign-in: the site's own logins
|
|
39
|
+
|
|
40
|
+
- A site's members sign in through its auth kit. The collection's kit is `collection.defaultAuthKitId`
|
|
41
|
+
(`SL.collection.get(collectionId)`).
|
|
42
|
+
- A club should have **its own logins**, so its members are accounts of this site only. In Forge: the
|
|
43
|
+
Site tab → Members → "Set up member sign-in" (or "Use my own members" on a site that has the shared
|
|
44
|
+
login). That creates the kit if needed, makes it the site's, and switches it to its own members.
|
|
45
|
+
- Use the flows in auth-kit.md: register, login, magic link, Google, password reset, email verification.
|
|
46
|
+
Never build a login of your own, and never store passwords.
|
|
47
|
+
- On sign-in, the SDK keeps the member's session (`setBearerToken`). Persist `session.token`
|
|
48
|
+
(respect `rememberMe`), restore it on load with `SL.setBearerToken(token)`, and clear it on sign-out.
|
|
49
|
+
|
|
50
|
+
By default anyone can create a login, and **being a member is the level, not the login**: an approval
|
|
51
|
+
club is just "signed in, no level yet".
|
|
52
|
+
|
|
53
|
+
**Invitation only.** A site with its own logins can turn sign-up off: `SL.authKit.update(collectionId,
|
|
54
|
+
authKitId, { signUp: 'invite' })` (or the owner, in the Customer Login app: Members → Who can join). Then nobody can
|
|
55
|
+
create an account by any route: password, magic link, phone, Google and the rest refuse with
|
|
56
|
+
`SIGN_UP_CLOSED`, and "email me a link" answers the same for strangers without sending anything. The
|
|
57
|
+
site's admins invite people instead:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
// admin session (the site's admin page, Studio): their account, their contact with a level, and an email
|
|
61
|
+
await SL.authKit.invite(collectionId, authKitId, {
|
|
62
|
+
email: "jo@example.com", firstName: "Jo",
|
|
63
|
+
customFields: { level: "member" },
|
|
64
|
+
redirectUrl: "https://club.example.com/sign-in", // the site's sign-in page (one of its own addresses)
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The email's link works once for 7 days and opens the sign-in page with `mode=magicLink&token=…`: handle
|
|
69
|
+
it with `SL.authKit.verifyMagicLink(clientId, token)`, as for any magic link. After that the member
|
|
70
|
+
signs in however the site offers (a magic link to their email, "forgot password" to set one, Google
|
|
71
|
+
with the same email). Build the sign-in page with no "Create an account" option when the kit's
|
|
72
|
+
`signUp` is `invite` (it's on the public config, `SL.authKit.load(authKitId)`), and say "Members only.
|
|
73
|
+
Ask the club for an invitation."
|
|
74
|
+
|
|
75
|
+
**Added by the staff first?** When someone signs in with a verified email or phone, their login is
|
|
76
|
+
attached to the contact the site already has for it: an invite, someone the staff added in Studio, a
|
|
77
|
+
form they filled in. So the level set on that contact is theirs from the first sign-in.
|
|
78
|
+
|
|
79
|
+
## 2. The level field
|
|
80
|
+
|
|
81
|
+
In a **website**, contact fields are part of its cloud blueprint (`cloud/contacts.json`, applied with
|
|
82
|
+
`forge-cloud`):
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"fields": {
|
|
87
|
+
"level": { "title": "Membership", "type": "string", "enum": ["pending", "member", "gold"], "public": "visible" },
|
|
88
|
+
"memberUntil": { "title": "Member until", "type": "date", "public": "visible" },
|
|
89
|
+
"showInDirectory": { "title": "Show me in the members' list", "type": "boolean", "public": "editable" }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
| `public` | Member can see it | Member can change it | Use for |
|
|
95
|
+
|---|---|---|---|
|
|
96
|
+
| `hidden` (default) | no | no | Staff notes, a paid-until the member shouldn't see |
|
|
97
|
+
| `visible` | yes | **no** | **The level**, an expiry date |
|
|
98
|
+
| `editable` | yes | yes | Preferences and consent the member owns |
|
|
99
|
+
|
|
100
|
+
A member's own reads and writes (`SL.contact.publicGetMine`, `SL.contact.publicUpdateMine`) and public
|
|
101
|
+
forms follow these settings on the server, so a `visible` level can't be changed from the browser.
|
|
102
|
+
|
|
103
|
+
Outside a website (a plain app), the same settings live in the collection's `contactCustomFields`
|
|
104
|
+
(field definitions, a SchemaFormConfig) and `contacts` settings groups (`publicVisibleFields`,
|
|
105
|
+
`publicEditableFields`, paths like `customFields.level`). Those lists replace what's stored: read
|
|
106
|
+
them, add to them, and write back the whole list, defaults included. Prefer letting the owner set
|
|
107
|
+
fields up in Studio → Customers.
|
|
108
|
+
|
|
109
|
+
## 3. Showing members their status
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const me = await SL.contact.publicGetMine(collectionId) // needs the member's session
|
|
113
|
+
// me.contact?.customFields?.level → "gold"; only fields the member may see are returned
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Use it for the "My membership" page and to decide what to **show**. It is not a lock: anything
|
|
117
|
+
members-only must come from a server function.
|
|
118
|
+
|
|
119
|
+
## 4. Members-only content: check in a server function
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
// visibility: public, authority: collection, elevated: true
|
|
123
|
+
// capabilities: sl:contacts:write (forCaller), sl:records:read
|
|
124
|
+
export async function memberNews(ctx, event) {
|
|
125
|
+
const me = await ctx.sl.contacts.forCaller() // null when signed out
|
|
126
|
+
if (!me) return { ok: false, reason: "sign-in" }
|
|
127
|
+
const level = me.customFields.level
|
|
128
|
+
if (level !== "member" && level !== "gold") return { ok: false, reason: "not-a-member" }
|
|
129
|
+
const until = me.customFields.memberUntil
|
|
130
|
+
if (until && String(until) < new Date().toISOString().slice(0, 10)) return { ok: false, reason: "expired" }
|
|
131
|
+
const news = await ctx.sl.appRecords.query({ recordType: "member-news" })
|
|
132
|
+
return { ok: true, level, news } // never return me.contactId or staff-only fields
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
- `forCaller()` returns the member's contact with its tags and **all** custom fields, staff-only ones
|
|
137
|
+
included. Decide with them and return only what the member may see.
|
|
138
|
+
- A site's `callFunction` sends the member's session with the call. Without it, the function sees an
|
|
139
|
+
anonymous caller. Check that `src/site/lib/call-function.ts` adds `authorization: Bearer <token>`
|
|
140
|
+
from `sl().getBearerToken()`; older sites may need it added.
|
|
141
|
+
- Do the same check in every function that acts for members: booking a court, entering a result,
|
|
142
|
+
claiming a discount.
|
|
143
|
+
|
|
144
|
+
## 5. Joining rules
|
|
145
|
+
|
|
146
|
+
| Club | How it works |
|
|
147
|
+
|---|---|
|
|
148
|
+
| **Open** (sign up = member) | Signed in is enough; no level needed, or a `level` the site never gates on |
|
|
149
|
+
| **Approval** ("sign up and I'll add you") | Anyone signs up; the owner sets `level` in Studio → Customers; until then the site says "We'll confirm your membership" |
|
|
150
|
+
| **Invite only** (no sign-up) | `signUp: 'invite'`; the owner invites people with their level (`SL.authKit.invite`): the Customer Login app's Members card, or an "Invite a member" screen on the site's admin page; the sign-in page has no sign-up |
|
|
151
|
+
| **Tiers** (silver, gold) | `level` with an `enum`; each gated function lists the levels it allows |
|
|
152
|
+
| **Paid** | By hand: the owner sets `level` (and `memberUntil`) once paid. Online: a memberships app or connector writes the same fields after payment |
|
|
153
|
+
|
|
154
|
+
Record joining as an interaction (`ctx.sl.interactions.record`, e.g. `member-joined`), and a
|
|
155
|
+
`cloud/flows.json` flow can welcome the member and tell the team someone is waiting for approval.
|
|
156
|
+
|
|
157
|
+
## 6. The owner's side
|
|
158
|
+
|
|
159
|
+
- **Studio → Customers**: everyone who signed up, their level, and when they joined. Filter by level
|
|
160
|
+
and set it on the contact.
|
|
161
|
+
- **Inviting**: the Customer Login app's Members card (Who can join, Invite a member), or the site's own
|
|
162
|
+
admin screen calling `SL.authKit.invite`; inviting someone again sends a fresh link.
|
|
163
|
+
- **Email a level**: a segment on `customFields.level`, sent from Studio (comms.md).
|
|
164
|
+
- **Tags** suit many-valued labels (`committee`, `coach`); a member has one `level`.
|
|
165
|
+
|
|
166
|
+
## Don't
|
|
167
|
+
|
|
168
|
+
- Keep members in app records, a table of your own, or the site's code. The contact is the member.
|
|
169
|
+
- Trust the browser's idea of the level. Check it in a server function.
|
|
170
|
+
- Let a member set a level that unlocks anything (`public: "editable"`).
|
|
171
|
+
- Return contact ids, other members' details, or staff-only fields to the browser.
|
|
172
|
+
- Build payments into a site. A memberships app or connector owns fees.
|
|
173
|
+
- Hide the sign-up button and call that invitation only. Set `signUp: 'invite'`, so the server refuses.
|
|
174
|
+
|
|
175
|
+
## Example: a tennis box league
|
|
176
|
+
|
|
177
|
+
- **Players** are members: own logins, `level` = `pending` or `player`, set by the organiser.
|
|
178
|
+
- **Fixtures, results and standings** are a custom app's records.
|
|
179
|
+
- Only a `player` can enter a result: the function checks `forCaller()`, then that the player is in
|
|
180
|
+
that fixture.
|
|
181
|
+
- **Standings** are public; contact details stay hidden.
|
|
182
|
+
- **New player signs up**: the flow emails the organiser, who sets them to `player` in Studio.
|