@providame/vue 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -46,6 +46,26 @@ import { SignIn } from '@providame/vue'
46
46
  </template>
47
47
  ```
48
48
 
49
+ ### Component props
50
+
51
+ `SignIn`, `SignUp`, and `ResetPassword` all accept `title`, `subtitle`, and
52
+ `logo`. `title` defaults to a branding-aware heading (e.g. "Sign in to Acme")
53
+ once your environment's app name loads; `logo` defaults to your configured
54
+ branding logo.
55
+
56
+ `SignUp` additionally accepts:
57
+
58
+ - `collect-name` (default `true`) — toggles the first/last name fields.
59
+ - `invite-token` — **required** when your environment's signup-policy mode is
60
+ `invite_only`: the single-use code from an admin-generated signup-invite
61
+ email. Read it from your own app's invite-accept URL and pass it through —
62
+ it threads directly to `CreateSignUpParams.inviteToken`. Every other
63
+ signup-policy mode ignores it if set.
64
+
65
+ ```vue
66
+ <SignUp :collect-name="false" invite-token="tok_..." />
67
+ ```
68
+
49
69
  ### Modal vs. page usage
50
70
 
51
71
  These components serve two contexts, and the footer link behaves differently
@@ -77,9 +97,284 @@ The listener takes precedence when both it and a URL are supplied.
77
97
  import { useSignIn } from '@providame/vue'
78
98
 
79
99
  const signIn = useSignIn()
100
+
101
+ // Preferred — one guarded call for the common identifier + password case.
80
102
  await signIn.signInWithPassword('user@example.com', 'password')
103
+
104
+ // Equivalent, longhand — useful if you need to do something between the two
105
+ // steps, e.g. show a separate password screen after the identifier is entered.
106
+ await signIn.create('user@example.com')
107
+ await signIn.attemptFirstFactor({ strategy: 'password', password: 'password' })
108
+
109
+ if (signIn.isComplete.value) {
110
+ // signIn.session.value is set
111
+ }
112
+ ```
113
+
114
+ ### Passwordless sign-in & MFA (`useSignIn`)
115
+
116
+ Beyond password sign-in, `useSignIn()` exposes passkey, email-OTP, SMS-OTP,
117
+ and magic-link sign-in, plus mid-flow TOTP enrollment. The prebuilt `<SignIn>`
118
+ above already wires password and passkey sign-in — email-OTP, SMS-OTP, and
119
+ magic-link are **headless-only** escape hatches; build your own UI around
120
+ them if you need one.
121
+
122
+ ```ts
123
+ import { useSignIn } from '@providame/vue'
124
+ const signIn = useSignIn()
125
+
126
+ // Passkey — opens the flow and immediately prompts navigator.credentials.get().
127
+ await signIn.signInWithPasskey(identifier, window.location.hostname)
128
+
129
+ // Email one-time code (first factor)
130
+ await signIn.signInWithOTPEmail(identifier)
131
+ await signIn.attemptFirstFactor({ strategy: 'otp_email', code })
132
+ await signIn.resendOTPEmail()
133
+
134
+ // SMS one-time code (first factor) — identifier is still email; the code is
135
+ // texted to whatever phone number is on file for that account.
136
+ await signIn.signInWithOTPSMS(identifier)
137
+ await signIn.attemptFirstFactor({ strategy: 'otp_sms', code })
138
+ await signIn.resendOTPSMS()
139
+
140
+ // Magic link — emails a passwordless "click to sign in" link pointing at returnUrl.
141
+ await signIn.signInWithMagicLink(identifier, 'https://myapp.com/auth/callback')
142
+
143
+ // Mid-flow TOTP, reached from any of the strategies above.
144
+ if (signIn.status.value === 'needs_second_factor') {
145
+ await signIn.attemptSecondFactor({ strategy: 'totp', code })
146
+ }
147
+ if (signIn.status.value === 'needs_mfa_enrollment') {
148
+ await signIn.enrollTotp() // -> signIn.enrollment.value = { secret, otpauthUri }
149
+ await signIn.verifyTotpEnrollment(code)
150
+ }
151
+
152
+ signIn.reset() // back to "idle"
153
+ ```
154
+
155
+ `firstFactors`/`secondFactors` report what the backend will accept next;
156
+ `recoveryCodes` is populated right after a mid-flow TOTP enrollment.
157
+
158
+ ### Sign-up (`useSignUp`)
159
+
160
+ `useSignUp()` mirrors `useSignIn()` — same reactive-state shape, for
161
+ registration. The prebuilt `<SignUp>` already wires all of this; reach for
162
+ the composable directly to build a fully custom sign-up UI.
163
+
164
+ ```ts
165
+ import { useSignUp } from '@providame/vue'
166
+ const signUp = useSignUp()
167
+
168
+ await signUp.create({
169
+ email,
170
+ password,
171
+ firstName, // optional
172
+ lastName, // optional
173
+ inviteToken, // required only when signup-policy mode is invite_only
174
+ turnstileToken, // required only when bot-check is enabled for this environment
175
+ })
176
+
177
+ if (signUp.needsVerification.value) {
178
+ await signUp.attemptVerification({ code })
179
+ }
180
+
181
+ // Post-registration second factor, if the environment's MFA policy requires one.
182
+ if (signUp.status.value === 'needs_second_factor') {
183
+ await signUp.attemptSecondFactor({ strategy: 'totp', code })
184
+ }
185
+
186
+ // Forced enrollment on a brand-new account.
187
+ if (signUp.status.value === 'needs_mfa_enrollment') {
188
+ await signUp.enrollTotp() // -> signUp.enrollment.value = { secret, otpauthUri }
189
+ await signUp.verifyTotpEnrollment(code)
190
+ }
191
+
192
+ if (signUp.isComplete.value) {
193
+ // signUp.session.value is set; signUp.recoveryCodes.value may carry backup codes
194
+ } else if (signUp.isWaitlisted.value) {
195
+ // verified, but this environment's signup-policy mode is "waitlist" — no session issued yet
196
+ }
197
+
198
+ signUp.reset()
199
+ ```
200
+
201
+ ### Password reset (`useResetPassword`, `<ResetPassword>`)
202
+
203
+ ```vue
204
+ <script setup>
205
+ import { ResetPassword } from '@providame/vue'
206
+ </script>
207
+
208
+ <template>
209
+ <ResetPassword sign-in-url="/sign-in" @complete="onComplete" />
210
+ </template>
211
+ ```
212
+
213
+ The prebuilt `<SignIn>` already embeds this whole flow behind its "Forgot
214
+ password?" link — mount `<ResetPassword>` on its own route only if you want
215
+ it reachable directly (e.g. from a password-reset email link).
216
+
217
+ ```ts
218
+ import { useResetPassword } from '@providame/vue'
219
+ const reset = useResetPassword()
220
+
221
+ await reset.create({ email })
222
+ await reset.attempt({ code, password }) // sets the new password and signs in
223
+
224
+ // Mid-flow TOTP, same shape as useSignIn/useSignUp.
225
+ if (reset.status.value === 'needs_second_factor') {
226
+ await reset.attemptSecondFactor({ strategy: 'totp', code })
227
+ }
228
+ if (reset.status.value === 'needs_mfa_enrollment') {
229
+ await reset.enrollTotp() // -> reset.enrollment.value = { secret, otpauthUri }
230
+ await reset.verifyTotpEnrollment(code)
231
+ }
232
+
233
+ await reset.resend() // re-send the reset code
234
+ reset.reset() // back to "idle"
81
235
  ```
82
236
 
237
+ `attempt()`/`attemptSecondFactor()`/`enrollTotp()`/`verifyTotpEnrollment()`/`resend()`
238
+ all throw if called before `create()` — there's no flow yet to act on.
239
+
240
+ ## What's in the box
241
+
242
+ **Components** — `SignIn`, `SignUp`, `ResetPassword`, `UserButton`, `UserProfile`,
243
+ `SignInButton`, `SignUpButton`, `SignOutButton`, `SignedIn`, `SignedOut`,
244
+ `Protect`, `RedirectToSignIn`, `OrganizationSwitcher`, `OrganizationProfile`,
245
+ `CreateOrganization`.
246
+
247
+ **Composables** — `useAuth`, `useUser`, `useSession`, `useSignIn`, `useSignUp`,
248
+ `useSignInIdp`, `useResetPassword`, `useOrganization`, `useOrganizationList`,
249
+ `useBranding`, `usePasskeyRegistrationLink`, `useProvidame`.
250
+
251
+ Every composable returns `Ref`s — including the functions, matching Clerk's own
252
+ Vue SDK. So it's `await getToken.value()`, not `getToken()`.
253
+
254
+ ## Organizations
255
+
256
+ ```vue
257
+ <script setup>
258
+ import { OrganizationSwitcher, OrganizationProfile, useOrganization } from '@providame/vue'
259
+
260
+ // Reads token claims — makes no request
261
+ const { organization, roles, permissions, has } = useOrganization()
262
+ </script>
263
+
264
+ <template>
265
+ <OrganizationSwitcher />
266
+ <OrganizationProfile />
267
+ </template>
268
+ ```
269
+
270
+ `useOrganization()` covers the **active** organization only and never fetches —
271
+ the token already carries `org_id`/`org_slug`/`org_roles`/`org_permissions`.
272
+ `useOrganizationList()` does fetch, since the full membership list isn't in a
273
+ token; it also exposes `setActive(orgId)`.
274
+
275
+ `<OrganizationProfile>` gates each action on the caller's own `org:sys_*`
276
+ permissions rather than a role name, so it works with roles you define yourself.
277
+ Pass `organization-id` to render a specific organization instead of the
278
+ session's active one.
279
+
280
+ `<OrganizationSwitcher>` accepts `hide-create` (drop "Create organization",
281
+ for apps that provision organizations centrally), `personal-label` (relabel
282
+ the no-organization option — Clerk calls it "Personal account"), and
283
+ `hide-personal` (drop that option entirely, for a B2B-only app where every
284
+ user belongs to an organization).
285
+
286
+ ## Gating UI
287
+
288
+ ```vue
289
+ <Protect role="admin">...</Protect>
290
+ <Protect org-permission="org:sys_memberships:manage">...</Protect>
291
+ <Protect role="admin" org-role="org:admin">...</Protect> <!-- both must pass -->
292
+ ```
293
+
294
+ `role`/`permission` gate on environment-level grants; `org-role`/`org-permission`
295
+ gate on the active organization. Omitted props are not constraints. **This is UI
296
+ gating only** — real enforcement is your backend verifying the token, via
297
+ `@providame/backend`.
298
+
299
+ ## Object-scoped access
300
+
301
+ `<Protect>` reads claims already in the token — synchronous, free, never fails.
302
+ `<Can>` asks a question about one specific resource — a network call, so it
303
+ needs a loading state and can fail:
304
+
305
+ ```vue
306
+ <Can permission="edit" resource="document:42">
307
+ <template #loading><Spinner /></template>
308
+ <template #fallback>You can't edit this document.</template>
309
+ <button>Edit</button>
310
+ </Can>
311
+ ```
312
+
313
+ Use `<Protect role="admin">` for "is this user an admin"; use `<Can permission="edit" resource="document:42">`
314
+ for "can this user edit *this* document". `<Can>` fails **closed** — a network
315
+ error renders `#fallback`, never the default slot. `useCan({ permission, resource })`
316
+ is the headless equivalent, returning `{ allowed, isLoading, error }`.
317
+
318
+ Answers are cached per-question for the life of the session (cleared on
319
+ sign-in/out and organization switch) — mounting `<Can>` for the same resource
320
+ twice on one page costs one request, not two.
321
+
322
+ `<Can>` also accepts an optional `context` prop (a plain object) that passes
323
+ extra values through for conditional/attribute-based permissions — e.g.
324
+ checking a permission that's only granted below a certain amount or within a
325
+ certain region.
326
+
327
+ ## Branding & the underlying client
328
+
329
+ `useBranding()` is called internally by every prebuilt component above —
330
+ reach for it directly only if you're building a fully custom UI and want the
331
+ same environment-configured colors/logo applied to it:
332
+
333
+ ```ts
334
+ import { useBranding } from '@providame/vue'
335
+
336
+ // Fetched once and memoized on the client instance — mounting several
337
+ // components that call this on one page costs a single request, not one
338
+ // per component. Also applies the colors as global CSS custom properties
339
+ // (--providame-accent/-bg/-fg and their -dark counterparts).
340
+ const branding = useBranding() // Ref<Branding>
341
+
342
+ branding.value.logoUrl // string | undefined
343
+ branding.value.primaryColor
344
+ branding.value.appName
345
+ ```
346
+
347
+ `useProvidame()` returns the underlying `Providame` client instance every
348
+ composable in this package is built on — every other composable calls this
349
+ internally, so you only need it directly to reach a client method this
350
+ package doesn't already wrap:
351
+
352
+ ```ts
353
+ import { useProvidame } from '@providame/vue'
354
+
355
+ const providame = useProvidame()
356
+
357
+ await providame.configuration() // { turnstileSiteKey, tosUrl, privacyUrl, helpUrl, supportEmail, ... }
358
+ await providame.branding() // same data useBranding() applies automatically
359
+ providame.organizations // OrganizationsApi — used internally by <OrganizationProfile>
360
+ ```
361
+
362
+ ## Theming
363
+
364
+ Every component accepts an `appearance` prop:
365
+
366
+ ```vue
367
+ <SignIn :appearance="{ variables: { colorPrimary: '#111827' } }" />
368
+ ```
369
+
370
+ Components also fetch your environment's configured branding (colors, logo,
371
+ light and dark) on mount and apply it automatically, so an unstyled `appearance`
372
+ prop is usually unnecessary.
373
+
374
+ ## Docs
375
+
376
+ Full reference: `/docs/sdk/vue` in your Providame dashboard.
377
+
83
378
  ## Build
84
379
 
85
380
  ```bash
@@ -0,0 +1,22 @@
1
+ type __VLS_Props = {
2
+ permission: string;
3
+ resource?: string;
4
+ context?: Record<string, unknown>;
5
+ };
6
+ declare var __VLS_1: {}, __VLS_3: {}, __VLS_5: {};
7
+ type __VLS_Slots = {} & {
8
+ loading?: (props: typeof __VLS_1) => any;
9
+ } & {
10
+ default?: (props: typeof __VLS_3) => any;
11
+ } & {
12
+ fallback?: (props: typeof __VLS_5) => any;
13
+ };
14
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
15
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
16
+ declare const _default: typeof __VLS_export;
17
+ export default _default;
18
+ type __VLS_WithSlots<T, S> = T & {
19
+ new (): {
20
+ $slots: S;
21
+ };
22
+ };
@@ -0,0 +1,14 @@
1
+ import "../styles.css";
2
+ import { type Appearance } from "@providame/core";
3
+ type __VLS_Props = {
4
+ appearance?: Appearance;
5
+ };
6
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
7
+ created: (organizationId: string) => any;
8
+ cancel: () => any;
9
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
10
+ onCreated?: ((organizationId: string) => any) | undefined;
11
+ onCancel?: (() => any) | undefined;
12
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
+ declare const _default: typeof __VLS_export;
14
+ export default _default;
@@ -0,0 +1,14 @@
1
+ import "../styles.css";
2
+ import { type Appearance } from "@providame/core";
3
+ type __VLS_Props = {
4
+ appearance?: Appearance;
5
+ /** Defaults to the session's ACTIVE organization. */
6
+ organizationId?: string;
7
+ };
8
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
9
+ close: () => any;
10
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
11
+ onClose?: (() => any) | undefined;
12
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
+ declare const _default: typeof __VLS_export;
14
+ export default _default;
@@ -0,0 +1,21 @@
1
+ import "../styles.css";
2
+ import { type Appearance } from "@providame/core";
3
+ type __VLS_Props = {
4
+ appearance?: Appearance;
5
+ /** Hide the "Create organization" action for apps that provision centrally. */
6
+ hideCreate?: boolean;
7
+ /**
8
+ * Label for the no-organization option. Clerk calls this "Personal
9
+ * account"; a B2B app with no personal mode can pass its own wording or
10
+ * drop the option entirely with `hidePersonal`.
11
+ */
12
+ personalLabel?: string;
13
+ hidePersonal?: boolean;
14
+ };
15
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
16
+ change: (organizationId: string | null) => any;
17
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
18
+ onChange?: ((organizationId: string | null) => any) | undefined;
19
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
20
+ declare const _default: typeof __VLS_export;
21
+ export default _default;
@@ -1,5 +1,8 @@
1
1
  type __VLS_Props = {
2
2
  role?: string;
3
+ permission?: string;
4
+ orgRole?: string;
5
+ orgPermission?: string;
3
6
  };
4
7
  declare var __VLS_1: {}, __VLS_3: {};
5
8
  type __VLS_Slots = {} & {
@@ -0,0 +1,16 @@
1
+ import { type Ref } from "vue";
2
+ import type { CanInput } from "@providame/core";
3
+ export interface UseCanResult {
4
+ /** null while the first check for the current input is still in flight. */
5
+ allowed: Ref<boolean | null>;
6
+ isLoading: Ref<boolean>;
7
+ error: Ref<Error | null>;
8
+ }
9
+ /** Watches a (possibly reactive) authorization question and keeps `allowed`
10
+ * in step with it, re-checking whenever the input changes.
11
+ *
12
+ * A guard against a superseded response overwriting a newer one: input can
13
+ * change again before a request resolves (e.g. a resource prop changing
14
+ * twice quickly), and without tracking which run is current, a slow first
15
+ * response could land after a faster second one and show stale state. */
16
+ export declare function useCan(input: Ref<CanInput>): UseCanResult;
@@ -0,0 +1,36 @@
1
+ import { type ComputedRef } from "vue";
2
+ export interface ActiveOrganizationInfo {
3
+ id: string;
4
+ slug: string;
5
+ name: string;
6
+ }
7
+ export interface UseOrganization {
8
+ /** False until the session's claims have loaded. */
9
+ isLoaded: ComputedRef<boolean>;
10
+ /** The active organization, or null when none is set. */
11
+ organization: ComputedRef<ActiveOrganizationInfo | null>;
12
+ /** Roles held in the active organization, across both tiers. */
13
+ roles: ComputedRef<string[]>;
14
+ /** Permissions resolved from those roles. */
15
+ permissions: ComputedRef<string[]>;
16
+ /** Whether the active organization grants a role. */
17
+ has: (params: {
18
+ role?: string;
19
+ permission?: string;
20
+ }) => boolean;
21
+ }
22
+ /**
23
+ * The session's ACTIVE organization, read straight from its access-token
24
+ * claims.
25
+ *
26
+ * Deliberately makes no network request: the token already carries
27
+ * org_id/org_slug/org_name/org_roles/org_permissions, so anything this
28
+ * composable could fetch would be strictly staler than what it already has.
29
+ * Use `useOrganizationList()` to enumerate every organization the user
30
+ * belongs to, or to switch which one is active.
31
+ *
32
+ * Display and gating only — these claims come from an unverified client-side
33
+ * decode. Real authorization belongs on the customer's own backend, which
34
+ * verifies the token's signature first.
35
+ */
36
+ export declare function useOrganization(): UseOrganization;
@@ -0,0 +1,29 @@
1
+ import { type Ref } from "vue";
2
+ import type { MyOrganization } from "@providame/core";
3
+ export interface UseOrganizationList {
4
+ /** False until the first load completes (successfully or not). */
5
+ isLoaded: Ref<boolean>;
6
+ /** Every organization the signed-in user belongs to, with their own roles. */
7
+ organizations: Ref<MyOrganization[]>;
8
+ /** The most recent load error, or null. */
9
+ error: Ref<Error | null>;
10
+ /** Re-fetches the list. */
11
+ reload: () => Promise<void>;
12
+ /**
13
+ * Switches the active organization and returns the freshly minted token's
14
+ * claims to the client, so subsequent `useOrganization()` reads reflect
15
+ * the new organization without waiting out the previous token's ~60s life.
16
+ * Pass null to clear.
17
+ */
18
+ setActive: (organizationId: string | null) => Promise<void>;
19
+ }
20
+ /**
21
+ * Every organization the signed-in user belongs to, plus the ability to
22
+ * switch which one is active.
23
+ *
24
+ * Unlike `useOrganization()` — which reads the active organization straight
25
+ * from token claims and makes no request — this fetches
26
+ * `GET /fapi/v1/me/organizations`, since the full membership list is not
27
+ * something a token carries.
28
+ */
29
+ export declare function useOrganizationList(): UseOrganizationList;
package/dist/index.d.ts CHANGED
@@ -5,9 +5,12 @@ export { useSignUp, type UseSignUp } from "./composables/useSignUp.js";
5
5
  export { useSession, type UseSession } from "./composables/useSession.js";
6
6
  export { useResetPassword, type UseResetPassword } from "./composables/useResetPassword.js";
7
7
  export { useAuth, type UseAuth } from "./composables/useAuth.js";
8
+ export { useOrganization, type ActiveOrganizationInfo, type UseOrganization, } from "./composables/useOrganization.js";
9
+ export { useOrganizationList, type UseOrganizationList, } from "./composables/useOrganizationList.js";
8
10
  export { useUser, type UseUser } from "./composables/useUser.js";
9
11
  export { useBranding } from "./composables/useBranding.js";
10
12
  export { usePasskeyRegistrationLink, type UsePasskeyRegistrationLink, } from "./composables/usePasskeyRegistrationLink.js";
13
+ export { useCan, type UseCanResult } from "./composables/useCan.js";
11
14
  export { default as SignIn } from "./components/SignIn.vue";
12
15
  export { default as SignUp } from "./components/SignUp.vue";
13
16
  export { default as ResetPassword } from "./components/ResetPassword.vue";
@@ -20,5 +23,9 @@ export { default as SignInButton } from "./components/SignInButton.vue";
20
23
  export { default as SignUpButton } from "./components/SignUpButton.vue";
21
24
  export { default as SignOutButton } from "./components/SignOutButton.vue";
22
25
  export { default as UserProfile } from "./components/UserProfile.vue";
26
+ export { default as OrganizationSwitcher } from "./components/OrganizationSwitcher.vue";
27
+ export { default as OrganizationProfile } from "./components/OrganizationProfile.vue";
28
+ export { default as CreateOrganization } from "./components/CreateOrganization.vue";
29
+ export { default as Can } from "./components/Can.vue";
23
30
  export type { Branding, Configuration, EnabledIdp, EndUserInfo, EnrollTotpResponse, FactorKind, FlowStatus, ProvidameOptions, MeUser, Session, } from "@providame/core";
24
31
  export { ProvidameError, verifyToken } from "@providame/core";