@proveanything/smartlinks 2.1.12 → 2.1.14
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 +17 -1
- package/dist/api/authKit.js +24 -0
- package/dist/docs/API_SUMMARY.md +120 -7
- package/dist/docs/auth-kit.md +50 -0
- package/dist/docs/connectors.md +42 -4
- package/dist/docs/entitlements.md +90 -0
- package/dist/docs/integrations.md +159 -9
- package/dist/docs/interactions.md +545 -540
- package/dist/docs/loyalty.md +17 -0
- package/dist/docs/server-functions.md +2 -0
- package/dist/openapi.yaml +258 -2
- package/dist/types/appManifest.d.ts +19 -1
- package/dist/types/authKit.d.ts +45 -1
- package/dist/types/integrations.d.ts +148 -3
- package/docs/API_SUMMARY.md +120 -7
- package/docs/auth-kit.md +50 -0
- package/docs/connectors.md +42 -4
- package/docs/entitlements.md +90 -0
- package/docs/integrations.md +159 -9
- package/docs/interactions.md +545 -540
- package/docs/loyalty.md +17 -0
- package/docs/server-functions.md +2 -0
- package/openapi.yaml +258 -2
- package/package.json +87 -87
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, 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";
|
|
1
|
+
import type { AuthLoginResponse, AppleLoginOptions, MicrosoftLoginOptions, 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*).
|
|
@@ -93,6 +93,22 @@ export declare namespace authKit {
|
|
|
93
93
|
* @see AppleLoginOptions
|
|
94
94
|
*/
|
|
95
95
|
function appleLogin(clientId: string, identityToken: string, opts?: AppleLoginOptions): Promise<AuthLoginResponse>;
|
|
96
|
+
/**
|
|
97
|
+
* Sign in with Microsoft — work or school accounts from any organisation, and personal Microsoft
|
|
98
|
+
* accounts — via a Microsoft ID token (public). Mirrors {@link appleLogin}: on success the bearer
|
|
99
|
+
* token is stored and the cache invalidated.
|
|
100
|
+
*
|
|
101
|
+
* Get the ID token from Microsoft's sign-in (response_type `id_token`, scope `openid profile email`)
|
|
102
|
+
* against the app in the public config's `microsoftClientId`. A site on its own domain can't be a
|
|
103
|
+
* registered address of that app, so the SmartLinks sign-in UI opens SmartLinks' Microsoft sign-in
|
|
104
|
+
* page in a popup, which calls this with `opts.returnOrigin` and hands the result back.
|
|
105
|
+
*
|
|
106
|
+
* Notable error codes: `MISSING_MICROSOFT_TOKEN` (400), `MICROSOFT_AUTH_NOT_CONFIGURED` (400),
|
|
107
|
+
* `INVALID_MICROSOFT_TOKEN` (401), `MICROSOFT_ORIGIN_NOT_ALLOWED` (403), `NOT_IN_ORGANISATION` (403 —
|
|
108
|
+
* the login setup's {@link AuthKitAccess} rule), `ACCOUNT_EXISTS_UNVERIFIED` (409),
|
|
109
|
+
* `MICROSOFT_AUTH_FAILED` (500). Gated by step-up MFA like every sign-in.
|
|
110
|
+
*/
|
|
111
|
+
function microsoftLogin(clientId: string, idToken: string, opts?: MicrosoftLoginOptions): Promise<AuthLoginResponse>;
|
|
96
112
|
/**
|
|
97
113
|
* Exchange a refresh token for a fresh access token (public — the refresh token IS
|
|
98
114
|
* the credential). **Native sessions only**; refresh tokens are issued only when the
|
package/dist/api/authKit.js
CHANGED
|
@@ -139,6 +139,30 @@ export var authKit;
|
|
|
139
139
|
return res;
|
|
140
140
|
}
|
|
141
141
|
authKit.appleLogin = appleLogin;
|
|
142
|
+
/**
|
|
143
|
+
* Sign in with Microsoft — work or school accounts from any organisation, and personal Microsoft
|
|
144
|
+
* accounts — via a Microsoft ID token (public). Mirrors {@link appleLogin}: on success the bearer
|
|
145
|
+
* token is stored and the cache invalidated.
|
|
146
|
+
*
|
|
147
|
+
* Get the ID token from Microsoft's sign-in (response_type `id_token`, scope `openid profile email`)
|
|
148
|
+
* against the app in the public config's `microsoftClientId`. A site on its own domain can't be a
|
|
149
|
+
* registered address of that app, so the SmartLinks sign-in UI opens SmartLinks' Microsoft sign-in
|
|
150
|
+
* page in a popup, which calls this with `opts.returnOrigin` and hands the result back.
|
|
151
|
+
*
|
|
152
|
+
* Notable error codes: `MISSING_MICROSOFT_TOKEN` (400), `MICROSOFT_AUTH_NOT_CONFIGURED` (400),
|
|
153
|
+
* `INVALID_MICROSOFT_TOKEN` (401), `MICROSOFT_ORIGIN_NOT_ALLOWED` (403), `NOT_IN_ORGANISATION` (403 —
|
|
154
|
+
* the login setup's {@link AuthKitAccess} rule), `ACCOUNT_EXISTS_UNVERIFIED` (409),
|
|
155
|
+
* `MICROSOFT_AUTH_FAILED` (500). Gated by step-up MFA like every sign-in.
|
|
156
|
+
*/
|
|
157
|
+
async function microsoftLogin(clientId, idToken, opts) {
|
|
158
|
+
const res = await post(`/authkit/${encodeURIComponent(clientId)}/auth/microsoft`, Object.assign({ idToken }, opts));
|
|
159
|
+
if (res.token) {
|
|
160
|
+
setBearerToken(res.token);
|
|
161
|
+
invalidateCache();
|
|
162
|
+
}
|
|
163
|
+
return res;
|
|
164
|
+
}
|
|
165
|
+
authKit.microsoftLogin = microsoftLogin;
|
|
142
166
|
/* ===================================
|
|
143
167
|
* Native session refresh (public)
|
|
144
168
|
* =================================== */
|
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.14 | Generated: 2026-11-09T13:01:05.900Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -64,8 +64,9 @@ For detailed guides on specific features:
|
|
|
64
64
|
- **[Analytics](analytics.md)** - Web analytics, link-click tracking, QR/tag scan telemetry, and event reporting
|
|
65
65
|
- **[Analytics Metadata Conventions](analytics-metadata-conventions.md)** - Standard recommended keys and conventions for analytics metadata fields
|
|
66
66
|
- **[Loyalty: Points, Members & Earning Rules](loyalty.md)** - Loyalty schemes, automatic point earning via interaction rules, member balances, transaction history, and manual adjustments
|
|
67
|
-
- **[Integrations](integrations.md)** - Inbound/outbound integration flows + the sealed-secret store; triggers (manual/event/schedule),
|
|
68
|
-
- **[Connectors](connectors.md)** - Connector apps (adapters for one external provider): a manifest `connector` block + server functions (testConnection/describeTarget/sample/fetch/shape/serialize/send); core does the mapping
|
|
67
|
+
- **[Integrations](integrations.md)** - Inbound/outbound integration flows + the sealed-secret store; triggers (manual/event/schedule), event patterns, the event envelope (interaction/contact/event sources), identity policy, event mapping, signed deliveries, contact as a target, field mappings
|
|
68
|
+
- **[Connectors](connectors.md)** - Connector apps (adapters for one external provider): a manifest `connector` block + server functions (testConnection/describeTarget/sample/fetch/shape/serialize/send/describeEvents/lookup); loyalty and CRM connectors; core does the mapping
|
|
69
|
+
- **[Entitlements](entitlements.md)** - "Is this person gold or above?" answered on demand from signed claims, a connector lookup (cached), or the contact — never a mirrored member base; gates on interaction types and in server functions
|
|
69
70
|
- **[AI Tools & Skills](ai-tools-and-skills.md)** - The AI capability catalog: skills apps invoke by name (e.g. research.brand), the tools the AI reaches for (web fetch/screenshot/brand assets/image gen), the agent loop, and how apps discover them
|
|
70
71
|
- **[Deep Link Discovery](deep-link-discovery.md)** - Registering and discovering navigable app states for portal menus and AI orchestration
|
|
71
72
|
- **[AI Guide Template](ai-guide-template.md)** - A sample for an app on how to build an AI setup guide
|
|
@@ -3129,11 +3130,13 @@ interface AppConnectorDeclaration {
|
|
|
3129
3130
|
serialize?: string
|
|
3130
3131
|
send?: string
|
|
3131
3132
|
fetch?: string
|
|
3133
|
+
describeEvents?: string
|
|
3134
|
+
lookup?: string
|
|
3132
3135
|
}
|
|
3133
3136
|
panels?: ConnectorPanel[]
|
|
3134
3137
|
aiGuide?: string
|
|
3135
3138
|
recordId?: string
|
|
3136
|
-
limits?: { requestsPerSecond?: number }
|
|
3139
|
+
limits?: { requestsPerSecond?: number; maxEvents?: number; maxAttributes?: number }
|
|
3137
3140
|
}
|
|
3138
3141
|
```
|
|
3139
3142
|
|
|
@@ -4624,6 +4627,24 @@ interface LogoutResponse {
|
|
|
4624
4627
|
}
|
|
4625
4628
|
```
|
|
4626
4629
|
|
|
4630
|
+
**MicrosoftLoginOptions** (interface)
|
|
4631
|
+
```typescript
|
|
4632
|
+
interface MicrosoftLoginOptions {
|
|
4633
|
+
returnOrigin?: string
|
|
4634
|
+
trustedDeviceToken?: string
|
|
4635
|
+
}
|
|
4636
|
+
```
|
|
4637
|
+
|
|
4638
|
+
**AuthKitAccess** (interface)
|
|
4639
|
+
```typescript
|
|
4640
|
+
interface AuthKitAccess {
|
|
4641
|
+
mode: 'anyone' | 'organisation'
|
|
4642
|
+
domains?: string[]
|
|
4643
|
+
emails?: string[]
|
|
4644
|
+
microsoftTenants?: Record<string, string>
|
|
4645
|
+
}
|
|
4646
|
+
```
|
|
4647
|
+
|
|
4627
4648
|
**AppleLoginOptions** (interface)
|
|
4628
4649
|
```typescript
|
|
4629
4650
|
interface AppleLoginOptions {
|
|
@@ -4953,6 +4974,8 @@ interface AuthKitConfig {
|
|
|
4953
4974
|
collectionIds?: string[]
|
|
4954
4975
|
identity?: AuthKitIdentity
|
|
4955
4976
|
signUp?: 'open' | 'invite'
|
|
4977
|
+
access?: AuthKitAccess
|
|
4978
|
+
microsoftClientId?: string
|
|
4956
4979
|
}
|
|
4957
4980
|
```
|
|
4958
4981
|
|
|
@@ -7933,9 +7956,30 @@ interface FlowConnectorRef {
|
|
|
7933
7956
|
}
|
|
7934
7957
|
```
|
|
7935
7958
|
|
|
7959
|
+
**EventMappingRule** (interface)
|
|
7960
|
+
```typescript
|
|
7961
|
+
interface EventMappingRule {
|
|
7962
|
+
id?: string
|
|
7963
|
+
match?: { types?: string[]; interactionIds?: string[]; outcomes?: string[]; appIds?: string[] }
|
|
7964
|
+
event: string
|
|
7965
|
+
attributes?: Record<string, string | number | boolean | { path: string; default?: unknown } | { value: unknown }>
|
|
7966
|
+
}
|
|
7967
|
+
```
|
|
7968
|
+
|
|
7969
|
+
**EventMapping** (interface)
|
|
7970
|
+
```typescript
|
|
7971
|
+
interface EventMapping {
|
|
7972
|
+
rules: EventMappingRule[]
|
|
7973
|
+
unmatched?: 'skip' | 'passthrough'
|
|
7974
|
+
}
|
|
7975
|
+
```
|
|
7976
|
+
|
|
7936
7977
|
**IntegrationFlowConfig** (interface)
|
|
7937
7978
|
```typescript
|
|
7938
7979
|
interface IntegrationFlowConfig {
|
|
7980
|
+
signing?: { credentialRef: string; previousCredentialRef?: string }
|
|
7981
|
+
identity?: { policy?: IdentityPolicy; hashKeyCredentialRef?: string }
|
|
7982
|
+
eventMapping?: EventMapping
|
|
7939
7983
|
connection?: FlowConnection
|
|
7940
7984
|
fieldMappings?: FieldMapping[]
|
|
7941
7985
|
connector?: FlowConnectorRef
|
|
@@ -7955,8 +7999,8 @@ interface IntegrationFlow {
|
|
|
7955
7999
|
status: FlowStatus
|
|
7956
8000
|
eventTypes: string[]
|
|
7957
8001
|
schedule: string | null
|
|
7958
|
-
sourceEntity: string | null
|
|
7959
|
-
targetEntity: string | null
|
|
8002
|
+
sourceEntity: FlowSourceEntity | string | null
|
|
8003
|
+
targetEntity: FlowTargetEntity | string | null
|
|
7960
8004
|
config: IntegrationFlowConfig
|
|
7961
8005
|
createdBy: string | null
|
|
7962
8006
|
createdAt: string
|
|
@@ -8145,6 +8189,66 @@ interface SaveConnectorInstallationInput {
|
|
|
8145
8189
|
}
|
|
8146
8190
|
```
|
|
8147
8191
|
|
|
8192
|
+
**ConnectorEventsSchema** (interface)
|
|
8193
|
+
```typescript
|
|
8194
|
+
interface ConnectorEventsSchema {
|
|
8195
|
+
events: Array<{ name: string; title?: string; attributes?: Array<{ name: string; type?: string; required?: boolean }> }>
|
|
8196
|
+
limits?: { maxEvents?: number; maxAttributes?: number }
|
|
8197
|
+
}
|
|
8198
|
+
```
|
|
8199
|
+
|
|
8200
|
+
**ConnectorLookupInput** (interface)
|
|
8201
|
+
```typescript
|
|
8202
|
+
interface ConnectorLookupInput {
|
|
8203
|
+
installation: ConnectorInstallation
|
|
8204
|
+
identity: { externalId?: string | null; email?: string | null; phone?: string | null; emailHash?: string | null; phoneHash?: string | null; contactId?: string | null }
|
|
8205
|
+
want?: string[]
|
|
8206
|
+
}
|
|
8207
|
+
```
|
|
8208
|
+
|
|
8209
|
+
**ConnectorLookupResult** (interface)
|
|
8210
|
+
```typescript
|
|
8211
|
+
interface ConnectorLookupResult {
|
|
8212
|
+
found: boolean
|
|
8213
|
+
memberId?: string | null
|
|
8214
|
+
tier?: string | null
|
|
8215
|
+
points?: number | null
|
|
8216
|
+
status?: string | null
|
|
8217
|
+
attributes?: Record<string, unknown>
|
|
8218
|
+
asOf?: string | null
|
|
8219
|
+
}
|
|
8220
|
+
```
|
|
8221
|
+
|
|
8222
|
+
**EventEnvelope** (interface)
|
|
8223
|
+
```typescript
|
|
8224
|
+
interface EventEnvelope {
|
|
8225
|
+
id: string
|
|
8226
|
+
type: string
|
|
8227
|
+
entityType: string | null
|
|
8228
|
+
entityId: string | null
|
|
8229
|
+
occurredAt: string
|
|
8230
|
+
collectionId: string
|
|
8231
|
+
orgId: string
|
|
8232
|
+
appId: string | null
|
|
8233
|
+
identity: {
|
|
8234
|
+
contactId: string | null
|
|
8235
|
+
userId: string | null
|
|
8236
|
+
externalIds: Record<string, string>
|
|
8237
|
+
emailHash?: string | null
|
|
8238
|
+
phoneHash?: string | null
|
|
8239
|
+
hashAlgorithm?: 'hmac-sha256' | null
|
|
8240
|
+
email?: string | null
|
|
8241
|
+
phone?: string | null
|
|
8242
|
+
}
|
|
8243
|
+
identityPolicy: IdentityPolicy
|
|
8244
|
+
product: { id: string; name?: string | null; sku?: string | null; gtin?: string | null; variantId?: string | null; batchId?: string | null } | null
|
|
8245
|
+
proof: { id: string } | null
|
|
8246
|
+
subject: Record<string, unknown>
|
|
8247
|
+
event?: { name: string; attributes: Record<string, unknown>; rule: string | null }
|
|
8248
|
+
attributes: Record<string, unknown>
|
|
8249
|
+
}
|
|
8250
|
+
```
|
|
8251
|
+
|
|
8148
8252
|
**CapturedRequest** (interface)
|
|
8149
8253
|
```typescript
|
|
8150
8254
|
interface CapturedRequest {
|
|
@@ -8347,9 +8451,15 @@ interface RecordTypesResponse {
|
|
|
8347
8451
|
|
|
8348
8452
|
**FlowAuthMethod** = `'api_key' | 'bearer' | 'basic' | 'webhook' | 'oauth2' | 'none'`
|
|
8349
8453
|
|
|
8454
|
+
**IdentityPolicy** = `'externalIds' | 'hashed' | 'raw'`
|
|
8455
|
+
|
|
8456
|
+
**FlowSourceEntity** = `'product' | 'interaction' | 'contact' | 'event'`
|
|
8457
|
+
|
|
8458
|
+
**FlowTargetEntity** = `'product' | 'contact'`
|
|
8459
|
+
|
|
8350
8460
|
**UpdateFlowInput** = `Partial<Omit<CreateFlowInput, 'direction'>> & {`
|
|
8351
8461
|
|
|
8352
|
-
**ConnectorConsoleStep** = `'testConnection' | 'describeTarget' | 'sample' | 'shape' | 'serialize'`
|
|
8462
|
+
**ConnectorConsoleStep** = `'testConnection' | 'describeTarget' | 'sample' | 'shape' | 'serialize' | 'describeEvents' | 'lookup'`
|
|
8353
8463
|
|
|
8354
8464
|
**RunTrigger** = `'manual' | 'event' | 'schedule' | 'test'`
|
|
8355
8465
|
|
|
@@ -11937,6 +12047,9 @@ Sign in with LinkedIn (OpenID Connect) via a server-side authorization code (pub
|
|
|
11937
12047
|
**appleLogin**(clientId: string, identityToken: string, opts?: AppleLoginOptions) → `Promise<AuthLoginResponse>`
|
|
11938
12048
|
Sign in with Apple via an Apple identity token (public). Mirrors {@link googleLogin}. On success the returned bearer token is stored automatically and the cache is invalidated. Notable error codes (thrown as `SmartlinksApiError`, read via `err.errorCode`): - `MISSING_APPLE_TOKEN` (400), `APPLE_AUTH_NOT_CONFIGURED` (400), `INVALID_APPLE_TOKEN` (401), `APPLE_AUTH_FAILED` (500) - `ACCOUNT_EXISTS_UNVERIFIED` (409) — an unverified account already owns this email; the server refuses to silently link. `err.details.requiresEmailVerification` is `true`. Recoverable: the user should sign in with their password (or reset it), then link Apple from settings. **The same 409 can now come back from {@link googleLogin}** under the shared verified-to-verified linking policy. Gated by step-up MFA — see {@link login} for the `MFA_REQUIRED` error shape. Pass `opts.trustedDeviceToken` to skip the challenge on a recognized device.
|
|
11939
12049
|
|
|
12050
|
+
**microsoftLogin**(clientId: string, idToken: string, opts?: MicrosoftLoginOptions) → `Promise<AuthLoginResponse>`
|
|
12051
|
+
Sign in with Microsoft — work or school accounts from any organisation, and personal Microsoft accounts — via a Microsoft ID token (public). Mirrors {@link appleLogin}: on success the bearer token is stored and the cache invalidated. Get the ID token from Microsoft's sign-in (response_type `id_token`, scope `openid profile email`) against the app in the public config's `microsoftClientId`. A site on its own domain can't be a registered address of that app, so the SmartLinks sign-in UI opens SmartLinks' Microsoft sign-in page in a popup, which calls this with `opts.returnOrigin` and hands the result back. Notable error codes: `MISSING_MICROSOFT_TOKEN` (400), `MICROSOFT_AUTH_NOT_CONFIGURED` (400), `INVALID_MICROSOFT_TOKEN` (401), `MICROSOFT_ORIGIN_NOT_ALLOWED` (403), `NOT_IN_ORGANISATION` (403 — the login setup's {@link AuthKitAccess} rule), `ACCOUNT_EXISTS_UNVERIFIED` (409), `MICROSOFT_AUTH_FAILED` (500). Gated by step-up MFA like every sign-in.
|
|
12052
|
+
|
|
11940
12053
|
**refreshToken**(clientId: string, refreshToken: string) → `Promise<RefreshResponse>`
|
|
11941
12054
|
Exchange a refresh token for a fresh access token (public — the refresh token IS the credential). **Native sessions only**; refresh tokens are issued only when the host opted in via `initializeApi({ platform: 'native' })`. On success the new access token is stored automatically (`setBearerToken`). The returned `refreshToken` is **rotated** — the caller must persist it and discard the old one before refreshing again. ⚠️ **Single-use, no retry, serialize calls.** This method issues exactly one request and never retries: replaying a consumed refresh token triggers `REFRESH_TOKEN_REUSE_DETECTED` (the whole session family is revoked). The caller is responsible for ensuring only one refresh is in flight at a time (e.g. across tabs or resume events). Errors (thrown as `SmartlinksApiError`, read via `err.errorCode`): `MISSING_REFRESH_TOKEN` (400), `INVALID_REFRESH_TOKEN` (401), `REFRESH_TOKEN_REUSE_DETECTED` (401) — the last two mean a hard logout.
|
|
11942
12055
|
|
package/dist/docs/auth-kit.md
CHANGED
|
@@ -259,6 +259,56 @@ try {
|
|
|
259
259
|
> ⚠️ This is a behaviour change for `googleLogin`, which previously merged silently in
|
|
260
260
|
> this case. Handle the 409 for both methods.
|
|
261
261
|
|
|
262
|
+
### Sign in with Microsoft
|
|
263
|
+
|
|
264
|
+
Work or school accounts from **any** organisation, and personal Microsoft accounts (Outlook, Hotmail,
|
|
265
|
+
Live). Add `'microsoft'` to `enabledProviders` — that's all: SmartLinks' own Microsoft app serves every
|
|
266
|
+
organisation, so neither you nor your users' IT registers anything.
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
const session = await authKit.microsoftLogin(clientId, microsoftIdToken, { returnOrigin });
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
- **In the SmartLinks sign-in UI** the Microsoft button opens SmartLinks' Microsoft sign-in page
|
|
273
|
+
(`https://smartlinks.app/apps/account/stable/microsoft-proxy.html`) in a popup — the one address
|
|
274
|
+
registered with Microsoft — which signs in and hands the session back to your site. Your site must be
|
|
275
|
+
one of the login setup's own (its collections' domains or site addresses, or `allowedRedirectDomains`),
|
|
276
|
+
else `MICROSOFT_ORIGIN_NOT_ALLOWED` (403).
|
|
277
|
+
- **Your own UI:** get an ID token from Microsoft (`response_type=id_token`, scope `openid profile email`,
|
|
278
|
+
authority `https://login.microsoftonline.com/common`) for the app in the public config's
|
|
279
|
+
`microsoftClientId`, and pass it here.
|
|
280
|
+
- **Some companies' IT approves new apps for their staff.** The first person from such a company sees
|
|
281
|
+
"needs admin approval"; their IT approves SmartLinks once for the organisation and everyone can sign in.
|
|
282
|
+
- **Your own name on Microsoft's screen (optional):** register your own *multitenant* app in Microsoft
|
|
283
|
+
Entra (Supported account types: any organisation + personal accounts; Web redirect URI as above, with
|
|
284
|
+
"ID tokens" ticked; token configuration: optional claim `xms_edov` on the ID token) and set its
|
|
285
|
+
Application (client) ID as `microsoftClientId`.
|
|
286
|
+
- A Microsoft account's email is trusted only when Microsoft vouches that its organisation owns the
|
|
287
|
+
domain (or for personal accounts): an organisation can give its users any address. Linking to an
|
|
288
|
+
existing account by email follows the same verified-to-verified rule as Google and Apple.
|
|
289
|
+
|
|
290
|
+
### Who can sign in (`access`)
|
|
291
|
+
|
|
292
|
+
Restrict a login setup to one organisation — no keys, no IT:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
await authKit.update(collectionId, clientId, {
|
|
296
|
+
access: { mode: 'organisation', domains: ['acme.com'], emails: ['contractor@gmail.com'] },
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
- Someone is from acme.com when their **verified** email is at acme.com, they're one of `emails`, or they
|
|
301
|
+
signed in with Microsoft from acme's own Microsoft organisation (looked up from the domain when you
|
|
302
|
+
save — `microsoftTenants` is filled in for you).
|
|
303
|
+
- Checked on **every** sign-in method each time a session is issued: everyone else gets
|
|
304
|
+
`NOT_IN_ORGANISATION` (403) with a message naming the domain. Tighten the rule and people already in
|
|
305
|
+
are refused from their next sign-in.
|
|
306
|
+
- `register` refuses outside addresses before making anything, and never signs an unverified address
|
|
307
|
+
straight in (even in `immediate` mode) — it must be verified first.
|
|
308
|
+
- Phone-only and WhatsApp-only accounts have no email to prove an organisation, so they can't get in.
|
|
309
|
+
- The public config (`load`) returns `access: { mode, domains }` so your page can say who it's for; the
|
|
310
|
+
`emails` list is never public. `{ mode: 'anyone' }` (or no `access`) lets everyone in, as before.
|
|
311
|
+
|
|
262
312
|
### Refresh tokens (native sessions)
|
|
263
313
|
|
|
264
314
|
Native/Capacitor hosts can hold long-lived sessions via refresh tokens. Opt in **once**
|
package/dist/docs/connectors.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Integration connectors
|
|
2
2
|
|
|
3
3
|
A **connector** is an app that knows one external provider (a PIM, a content network, a shop
|
|
4
|
-
platform…). It is installed on a collection like any app, versioned and released like any app, and
|
|
4
|
+
platform, a loyalty or CRM system…). It is installed on a collection like any app, versioned and released like any app, and
|
|
5
5
|
it adds a manifest `connector` block plus a handful of [server functions](server-functions.md) the
|
|
6
6
|
integration engine and the integrations console call by convention.
|
|
7
7
|
|
|
@@ -83,7 +83,7 @@ Types: `AppConnectorDeclaration` and friends in `@proveanything/smartlinks` (`sr
|
|
|
83
83
|
| `provider` | Stable provider key (`syndigo`, `oneworldsync`). |
|
|
84
84
|
| `title` | Provider display name; defaults to the app name. |
|
|
85
85
|
| `directions` | `inbound` (provider → SmartLinks), `outbound` (SmartLinks → provider), or both. |
|
|
86
|
-
| `entities` | SmartLinks entities it maps
|
|
86
|
+
| `entities` | SmartLinks entities it maps: `product`, and for people-shaped providers `interaction`, `contact`, `event` (see [Loyalty and CRM connectors](#loyalty-and-crm-connectors)). |
|
|
87
87
|
| `connection.fields[]` | The console's **Connect** form. A field with `secret` is stored as this app's secret of that name on the collection (write-only; never returned to the browser). Other fields are saved in the installation's connection settings. |
|
|
88
88
|
| `settings` | JSON Schema for installation settings; the console renders it as a form. |
|
|
89
89
|
| `presets[]` | Ready-made flows ("Products → provider"). A preset's `flow.config.fieldMappings` is a **suggested** mapping: the connector's knowledge of the provider, which the customer accepts or edits in core. |
|
|
@@ -91,7 +91,7 @@ Types: `AppConnectorDeclaration` and friends in `@proveanything/smartlinks` (`sr
|
|
|
91
91
|
| `panels[]` | Provider-specific screens the console mounts (an API explorer, a status page). |
|
|
92
92
|
| `aiGuide` | A guide the mapping assistant reads: the provider's quirks, required fields, good defaults. |
|
|
93
93
|
| `recordId` | Path of the provider's own id in a fetched record (e.g. `gtin`). Inbound records are landed and matched by it, so a re-import updates the same product instead of creating another. Default `id`; a flow can override it (`config.upsert.externalIdPath`). |
|
|
94
|
-
| `limits` | `{ requestsPerSecond }` toward the provider, enforced across every SmartLinks worker. |
|
|
94
|
+
| `limits` | `{ requestsPerSecond }` toward the provider, enforced across every SmartLinks worker; `{ maxEvents, maxAttributes }` when the provider caps registered events — a flow's `eventMapping` is checked against them when saved. |
|
|
95
95
|
|
|
96
96
|
---
|
|
97
97
|
|
|
@@ -111,7 +111,9 @@ declare the provider's hosts (`network:<host>`) and the secrets it reads (`secre
|
|
|
111
111
|
| `fetch` | Every inbound run | `ConnectorFetchInput` `{ installation, cursor, limit }` | `ConnectorRecords` `{ records, nextCursor }`: one page per call |
|
|
112
112
|
| `shape` | Every inbound run (and sample dry runs) | `ConnectorShapeInput` `{ installation, records }` | `ConnectorShapeResult` `{ documents, skipped }`: one provider-neutral document per record, same order; `null` skips one |
|
|
113
113
|
| `serialize` | Every outbound run | `ConnectorSerializeInput` `{ installation, documents, entities, targetOptions, eventType? }`: documents core mapped to `describeTarget` paths; `entities` = the SmartLinks record each came from (id, name); `targetOptions` = per-field options the mapping carries (e.g. locale, unit); a deletion arrives as `{ id, _deleted: true }` | `ConnectorSerializeResult` `{ payloads, skipped }`: one provider payload per document, same order; `null` skips one |
|
|
114
|
-
| `send` | Every outbound run | `ConnectorSendInput` `{ installation, payloads }` | `ConnectorSendResult` `{ results: [{ index, ok, status, error, externalId }] }` |
|
|
114
|
+
| `send` | Every outbound run | `ConnectorSendInput` `{ installation, payloads, eventType? }` | `ConnectorSendResult` `{ results: [{ index, ok, status, error, externalId, retryable?, retryAfterSeconds? }] }` — `retryable: false` for a provider validation error (dead at once), `true` / a 5xx or 429 status for anything worth retrying |
|
|
115
|
+
| `describeEvents` | Mapper opens; a flow with an `eventMapping` is saved | `{ installation }` | `ConnectorEventsSchema` `{ events: [{ name, title, attributes: [{ name, type, required }] }], limits: { maxEvents, maxAttributes } }`: the event names the provider has registered for this customer |
|
|
116
|
+
| `lookup` | An entitlement check (`docs/entitlements.md`) | `ConnectorLookupInput` `{ installation, identity: { externalId, email, phone } \| { externalId, emailHash, phoneHash }, want }` | `ConnectorLookupResult` `{ found, memberId, tier, points, status, attributes, asOf }`. Throw for an outage — never `found: false` |
|
|
115
117
|
|
|
116
118
|
**How a run flows:**
|
|
117
119
|
- **Inbound:** `fetch` (paged; every record is landed as received) → `shape` → core maps the documents
|
|
@@ -182,6 +184,41 @@ export async function shape(_ctx: any, event: any): Promise<ConnectorShapeResult
|
|
|
182
184
|
|
|
183
185
|
---
|
|
184
186
|
|
|
187
|
+
## Loyalty and CRM connectors
|
|
188
|
+
|
|
189
|
+
A provider that is about **people** rather than products uses the same contract with three more
|
|
190
|
+
pieces; nothing provider-specific lives in core. Full contract: `loyalty-connector-authoring.md` in the
|
|
191
|
+
platform's design docs; the shapes are in [integrations.md](integrations.md#event-sourced-flows-and-the-envelope).
|
|
192
|
+
|
|
193
|
+
| Flow | What core does | What the connector does |
|
|
194
|
+
|---|---|---|
|
|
195
|
+
| **Earn** (us → them) | an outbound flow with `sourceEntity: 'interaction' \| 'contact' \| 'event'` sends one **envelope** per event, through the flow's identity policy and `eventMapping`, signed, with retries and a run log | `describeEvents` (what the provider has registered), `serialize` (envelope → the provider's event payload), `send` if the generic HTTP destination can't make the call |
|
|
196
|
+
| **Profile** (them → us) | `ctx.sl.members.apply(document)` matches the contact and writes member id, tier, points, tags, external ids; or an inbound flow with `targetEntity: 'contact'` | an **HTTP-trigger function** receiving the provider's member webhook (verify their signature, then apply); or `fetch` + `shape` for a poll |
|
|
197
|
+
| **Gate / display** (on demand) | `entitlements.check` — claims → cached `lookup` → the contact's own loyalty fields → fallback; Valkey cache keyed on an identity hash, brand-set TTL | `lookup`: one identity → tier / points / status |
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
// A member webhook: the provider tells us about a person; we land it on the contact.
|
|
201
|
+
export default async function memberWebhook(event: any, ctx: any) {
|
|
202
|
+
verifyProviderSignature(event, await ctx.secrets.get('webhookSecret'))
|
|
203
|
+
const m = event.body
|
|
204
|
+
return ctx.sl.members.apply({
|
|
205
|
+
identity: { externalId: m.memberId, email: m.email },
|
|
206
|
+
contact: { firstName: m.firstName, lastName: m.lastName }, // fills empty fields only
|
|
207
|
+
loyalty: { memberId: m.memberId, tier: m.tier, points: m.points, status: m.status, asOf: m.updatedAt },
|
|
208
|
+
tags: { add: [`tier:${m.tier}`] },
|
|
209
|
+
}, { provider: 'acme-loyalty' }) // → { contactId, op: 'created' | 'updated' | 'unchanged' }
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Declare it `{ "trigger": { "type": "http" }, "capabilities": ["sl:members:write", "secrets:webhookSecret"] }`;
|
|
214
|
+
`sl:members:write` is approved by the admin on install. The envelope's `identity.externalIds[<provider>]`
|
|
215
|
+
is the member id this wrote back, so **Earn** can address the member by the provider's own id.
|
|
216
|
+
|
|
217
|
+
A connector should ship an `eventMapping` **preset** with the provider's standard event names and an
|
|
218
|
+
`aiGuide` paragraph, so the console's assistant proposes a customer's mapping in the provider's terms.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
185
222
|
## Where configuration lives
|
|
186
223
|
|
|
187
224
|
| What | Where |
|
|
@@ -220,6 +257,7 @@ await secrets.put(collectionId, 'provider-key', value, { appId }) /
|
|
|
220
257
|
await integrations.saveConnectorInstallation(collectionId, appId, { connection: { environment: 'production' } })
|
|
221
258
|
const test = await integrations.runConnectorStep(collectionId, appId, 'testConnection')
|
|
222
259
|
const target = await integrations.runConnectorStep(collectionId, appId, 'describeTarget')
|
|
260
|
+
const events = await integrations.runConnectorStep(collectionId, appId, 'describeEvents') // loyalty / engagement providers
|
|
223
261
|
const { records } = await integrations.runConnectorStep(collectionId, appId, 'sample', { limit: 5 })
|
|
224
262
|
const shaped = await integrations.runConnectorStep(collectionId, appId, 'shape', { records }) // dry run
|
|
225
263
|
const wire = await integrations.runConnectorStep(collectionId, appId, 'serialize', { documents }) // dry run
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Entitlements: "can this person do this?" without a mirror of their member base
|
|
2
|
+
|
|
3
|
+
A brand with twenty million members wants a game only Gold members can play. SmartLinks never copies
|
|
4
|
+
the member base to find out. A **gate** says what it needs — a predicate such as `tier>=gold` — and the
|
|
5
|
+
platform asks the collection's configured **sources**, in order, stopping at the first that answers.
|
|
6
|
+
The brand's system stays the record of truth; every answer says where it came from (`basis`) and how
|
|
7
|
+
old it is (`asOf`).
|
|
8
|
+
|
|
9
|
+
| Source | Data kept here | When it fits |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| **claims** | none | the brand's SSO or app issues a short-lived signed token carrying the tier; the caller presents it |
|
|
12
|
+
| **lookup** | a hashed key → a small answer, for minutes | the loyalty connector's `lookup` step answers for one person; cached in Valkey under an HMAC of the identity for the TTL the brand sets (default 15 min; "not a member" 5 min); one in-flight lookup per key; the connector's own rate limit applies |
|
|
13
|
+
| **contact** | what the contact already holds | `customFields.loyalty` / `loyaltyTier` written by the connector's member webhook (`ctx.sl.members.apply`) |
|
|
14
|
+
| **fallback** | — | when nothing answered: `deny` (default), `allow`, or `degrade` (allow, flagged `degraded: true`) |
|
|
15
|
+
|
|
16
|
+
Every decision is one structured log line — gate, predicate, allowed, basis, age, an identity **hash**
|
|
17
|
+
— and never an identifier. A caller sees `attributes` (tier, points, status, member) and never a member
|
|
18
|
+
id.
|
|
19
|
+
|
|
20
|
+
## Predicates
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
'tier>=gold' 'tier=silver' 'tier in gold,platinum' 'points>=500' 'member'
|
|
24
|
+
{ tier: { gte: 'gold' } } { tier: { in: ['gold', 'platinum'] } } { member: true }
|
|
25
|
+
{ attribute: { path: 'status', eq: 'active' } }
|
|
26
|
+
{ all: [p1, p2] } { any: [p1, p2] } { not: p }
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Tier comparisons use the collection's ordered `tiers` list (lowest first); a tier the list doesn't know
|
|
30
|
+
never satisfies an ordered comparison. A malformed predicate is a configuration error (`BAD_PREDICATE`),
|
|
31
|
+
never a silent denial.
|
|
32
|
+
|
|
33
|
+
## Where gates live
|
|
34
|
+
|
|
35
|
+
**An interaction type** — the public submit route checks it; the caller may present a claim:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
await SL.interactions.updateInteraction(collectionId, appId, typeId, {
|
|
39
|
+
permissions: { allowPublicSubmit: true, requires: { entitlement: 'tier>=gold', fallback: 'deny' } },
|
|
40
|
+
})
|
|
41
|
+
// A refused submit: 403 { error: 'FORBIDDEN', reason: 'entitlement_denied' }
|
|
42
|
+
// With a claim: body.entitlementToken = '<jwt>' or header X-SmartLinks-Entitlement: <jwt>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**A server function** — for the signed-in caller by default:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const d = await ctx.sl.entitlements.check('tier>=gold') // needs sl:entitlements:read
|
|
49
|
+
// → { allowed: true, basis: 'lookup-cache', asOf: '2026-11-08T09:00:00.000Z', attributes: { tier: 'gold', points: 1500, member: true } }
|
|
50
|
+
if (!d.allowed) return new Response(JSON.stringify({ error: 'gold_only' }), { status: 403 })
|
|
51
|
+
// Someone else, or a presented claim:
|
|
52
|
+
await ctx.sl.entitlements.check({ points: { gte: 500 } }, { identity: { email }, token, fallback: 'degrade' })
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Configuring a collection
|
|
56
|
+
|
|
57
|
+
The `entitlements` setting group (`settings.set(collectionId, 'entitlements', {...})`):
|
|
58
|
+
|
|
59
|
+
```jsonc
|
|
60
|
+
{
|
|
61
|
+
"sources": ["claims", "lookup", "contact"], // tried in this order
|
|
62
|
+
"fallback": "deny",
|
|
63
|
+
"tiers": ["bronze", "silver", "gold", "platinum"], // lowest first
|
|
64
|
+
"claims": {
|
|
65
|
+
"secretRef": "sec_brand_sso", // HS256 shared secret in the secret store …
|
|
66
|
+
"publicKeyPem": "-----BEGIN PUBLIC KEY-----…", // … or an RS256 / ES256 public key
|
|
67
|
+
"issuer": "https://login.brand.example", "audience": "smartlinks",
|
|
68
|
+
"tierClaim": "tier", "memberIdClaim": "sub", "attributesClaim": "entitlements", "maxAgeSec": 900
|
|
69
|
+
},
|
|
70
|
+
"lookup": {
|
|
71
|
+
"connectorAppId": "acme-loyalty", "provider": "acme-loyalty",
|
|
72
|
+
"ttlSec": 900, "negativeTtlSec": 300,
|
|
73
|
+
"identity": "raw", // what the connector's lookup receives: 'raw' (email/phone) or 'hashed'
|
|
74
|
+
"want": ["tier", "points", "status"]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The connector side (`lookup`) is one step: one identity in, `{ found, memberId, tier, points, status,
|
|
80
|
+
asOf }` out; throw on an outage so the next source is tried — see
|
|
81
|
+
[connectors.md](connectors.md#loyalty-and-crm-connectors).
|
|
82
|
+
|
|
83
|
+
## What a brand can be told
|
|
84
|
+
|
|
85
|
+
- We never need a copy of your member base: claims and lookups hold nothing, or a minutes-old answer per
|
|
86
|
+
active person.
|
|
87
|
+
- Where you want a local copy, it holds no identifiers (the HMAC mirror — on request).
|
|
88
|
+
- Your system remains the record of truth: every answer carries `asOf` and `basis`; TTLs are yours to set.
|
|
89
|
+
- You see every call: the connector's calls are rate-limited, logged, and leave from a fixed egress IP.
|
|
90
|
+
- Nothing leaves without a policy: the envelope's identity policy per flow.
|