@glydi/passkey-vue 0.1.0
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 +135 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.js +147 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# @glydi/passkey-vue
|
|
2
|
+
|
|
3
|
+
Thin Vue 3 adapter for the Glide passkey Web Component.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Glide is not yet published to npm. Install from a packed tarball or via `pnpm link`.
|
|
8
|
+
|
|
9
|
+
**Tarball (recommended):**
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@glydi/passkey-vue": "file:../glide/dist-packs/glydi-passkey-vue-0.1.0.tgz",
|
|
15
|
+
"@glydi/passkey-core": "file:../glide/dist-packs/glydi-passkey-core-0.1.0.tgz"
|
|
16
|
+
},
|
|
17
|
+
"pnpm": {
|
|
18
|
+
"overrides": {
|
|
19
|
+
"@glydi/passkey-core": "file:../glide/dist-packs/glydi-passkey-core-0.1.0.tgz"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The `pnpm.overrides` entry for `@glydi/passkey-core` prevents pnpm from trying to fetch
|
|
26
|
+
the transitive peer from the npm registry. See [docs/DISTRIBUTION.md](../../docs/DISTRIBUTION.md)
|
|
27
|
+
for full tarball and `pnpm link` instructions, including how to produce the tarballs.
|
|
28
|
+
|
|
29
|
+
> **Forthcoming:** `npm install @glydi/passkey-vue` will be the public form once the
|
|
30
|
+
> package is published. It is not yet available on npm.
|
|
31
|
+
|
|
32
|
+
## Minimal Usage
|
|
33
|
+
|
|
34
|
+
### `<PasskeyButton>` component
|
|
35
|
+
|
|
36
|
+
`PasskeyButton` is a `defineComponent` (not a `.vue` SFC). Import it from
|
|
37
|
+
`@glydi/passkey-vue` and use it in your template.
|
|
38
|
+
|
|
39
|
+
```vue
|
|
40
|
+
<!-- Source: packages/vue/src/PasskeyButton.ts -->
|
|
41
|
+
<script setup>
|
|
42
|
+
import { PasskeyButton } from "@glydi/passkey-vue";
|
|
43
|
+
</script>
|
|
44
|
+
<template>
|
|
45
|
+
<PasskeyButton
|
|
46
|
+
mode="signup"
|
|
47
|
+
label="Register passkey"
|
|
48
|
+
@success="(r) => console.log(r.user.id)"
|
|
49
|
+
@error="(e) => console.warn(e.code)"
|
|
50
|
+
/>
|
|
51
|
+
</template>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Headless `usePasskeyAuth` composable
|
|
55
|
+
|
|
56
|
+
For custom UI, use the headless composable — it returns an `elRef` to bind to a
|
|
57
|
+
`<biometric-auth-button>` alongside reactive state:
|
|
58
|
+
|
|
59
|
+
```vue
|
|
60
|
+
<script setup>
|
|
61
|
+
import { usePasskeyAuth } from "@glydi/passkey-vue";
|
|
62
|
+
|
|
63
|
+
const { elRef, phase, isPending, error, user, trigger } = usePasskeyAuth({
|
|
64
|
+
mode: "auto",
|
|
65
|
+
onSuccess: (r) => router.push("/app"),
|
|
66
|
+
});
|
|
67
|
+
</script>
|
|
68
|
+
<template>
|
|
69
|
+
<biometric-auth-button :ref="elRef" :label="isPending ? '…' : 'Sign in'" />
|
|
70
|
+
</template>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## API
|
|
74
|
+
|
|
75
|
+
### `PasskeyButton`
|
|
76
|
+
|
|
77
|
+
A Vue 3 component wrapping `<biometric-auth-button>`. Implemented via `defineComponent`
|
|
78
|
+
and an `h()` render function — not a `.vue` SFC.
|
|
79
|
+
|
|
80
|
+
**Props:**
|
|
81
|
+
|
|
82
|
+
| Prop | Type | Default | Description |
|
|
83
|
+
|------|------|---------|-------------|
|
|
84
|
+
| `mode` | `"auto" \| "signin" \| "signup"` | `"auto"` | Auth mode. See AuthMode callout below. |
|
|
85
|
+
| `username` | `string` | — | Username hint for registration/conditional UI. |
|
|
86
|
+
| `label` | `string` | — | Button label text. |
|
|
87
|
+
| `endpoints` | `Partial<GlideEndpoints>` | — | Override one or more API endpoint paths. |
|
|
88
|
+
| `fetchOptions` | `RequestInit` | — | Merged into every `fetch` call (e.g. custom headers). |
|
|
89
|
+
|
|
90
|
+
**Emits:**
|
|
91
|
+
|
|
92
|
+
| Event | Payload | Description |
|
|
93
|
+
|-------|---------|-------------|
|
|
94
|
+
| `success` | `AuthResult` | Emitted after successful authentication. |
|
|
95
|
+
| `error` | `GlideError` | Emitted when authentication fails. |
|
|
96
|
+
| `phasechange` | `string` | Emitted on every phase transition. Value is the new `AuthPhase`. |
|
|
97
|
+
|
|
98
|
+
> **Valid `mode` values are `"auto"`, `"signin"`, and `"signup"` only.** Using `"register"`
|
|
99
|
+
> or `"authenticate"` is invalid and will silently no-op.
|
|
100
|
+
|
|
101
|
+
### `usePasskeyAuth(options?)`
|
|
102
|
+
|
|
103
|
+
Headless Vue 3 composable. Returns a `UsePasskeyAuth` object:
|
|
104
|
+
|
|
105
|
+
| Return value | Type | Description |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `elRef` | `Ref<GlideElement \| null>` | Bind to element with `:ref="elRef"`. |
|
|
108
|
+
| `phase` | `Ref<AuthPhase>` | Current auth phase (`"idle"`, `"loading"`, `"success"`, etc.). |
|
|
109
|
+
| `isPending` | `ComputedRef<boolean>` | `true` during `"loading"` or `"authenticating"`. |
|
|
110
|
+
| `isUnsupported` | `ComputedRef<boolean>` | `true` when `phase === "unsupported"`. |
|
|
111
|
+
| `error` | `Ref<GlideError \| null>` | Last error, or `null`. |
|
|
112
|
+
| `user` | `Ref<GlideUser \| null>` | Authenticated user after success, or `null`. |
|
|
113
|
+
| `trigger()` | `() => void` | Imperatively start auth (same as clicking the button). |
|
|
114
|
+
| `reset()` | `() => void` | Reset phase and error back to `"idle"`. |
|
|
115
|
+
|
|
116
|
+
**`UsePasskeyAuthOptions`** (all optional):
|
|
117
|
+
|
|
118
|
+
| Option | Type | Description |
|
|
119
|
+
|--------|------|-------------|
|
|
120
|
+
| `mode` | `"auto" \| "signin" \| "signup"` | Auth mode. Default `"auto"`. |
|
|
121
|
+
| `username` | `string` | Username hint. |
|
|
122
|
+
| `endpoints` | `Partial<GlideEndpoints>` | Override API endpoint paths. |
|
|
123
|
+
| `fetchOptions` | `RequestInit` | Merged into every `fetch` call. |
|
|
124
|
+
| `onSuccess` | `(result: AuthResult) => void` | Success callback. |
|
|
125
|
+
| `onError` | `(error: GlideError) => void` | Error callback. |
|
|
126
|
+
|
|
127
|
+
### Re-exported types
|
|
128
|
+
|
|
129
|
+
`AuthMode`, `AuthPhase`, `AuthResult`, `GlideEndpoints`, `GlideError`, `GlideUser`
|
|
130
|
+
|
|
131
|
+
## Links
|
|
132
|
+
|
|
133
|
+
- [Root README](../../README.md) — architecture overview
|
|
134
|
+
- [Quickstart](../../docs/QUICKSTART.md) — full Next.js App Router integration walkthrough
|
|
135
|
+
- [Distribution guide](../../docs/DISTRIBUTION.md) — tarball and pnpm link install details
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as vue from 'vue';
|
|
2
|
+
import { PropType, Ref, ComputedRef } from 'vue';
|
|
3
|
+
import { AuthMode, GlideEndpoints, AuthResult, GlideError, AuthPhase, GlideUser } from '@glydi/passkey-core';
|
|
4
|
+
export { AuthMode, AuthPhase, AuthResult, GlideEndpoints, GlideError, GlideUser } from '@glydi/passkey-core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Thin declarative wrapper. This is intentionally tiny: all real work lives in
|
|
8
|
+
* the Web Component. The wrapper only:
|
|
9
|
+
* 1. ensures the element is registered (client-side, in onMounted),
|
|
10
|
+
* 2. forwards object props the DOM can't express as attributes, and
|
|
11
|
+
* 3. bridges CustomEvents → Vue emits.
|
|
12
|
+
*
|
|
13
|
+
* For full state (phase/error/user) use `usePasskeyAuth` instead.
|
|
14
|
+
*/
|
|
15
|
+
declare const PasskeyButton: vue.DefineComponent<{
|
|
16
|
+
mode: {
|
|
17
|
+
type: PropType<AuthMode>;
|
|
18
|
+
default: string;
|
|
19
|
+
};
|
|
20
|
+
username: {
|
|
21
|
+
type: StringConstructor;
|
|
22
|
+
default: undefined;
|
|
23
|
+
};
|
|
24
|
+
label: {
|
|
25
|
+
type: StringConstructor;
|
|
26
|
+
default: undefined;
|
|
27
|
+
};
|
|
28
|
+
endpoints: {
|
|
29
|
+
type: PropType<Partial<GlideEndpoints>>;
|
|
30
|
+
default: undefined;
|
|
31
|
+
};
|
|
32
|
+
fetchOptions: {
|
|
33
|
+
type: PropType<RequestInit>;
|
|
34
|
+
default: undefined;
|
|
35
|
+
};
|
|
36
|
+
}, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
|
|
37
|
+
[key: string]: any;
|
|
38
|
+
}>, unknown, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {
|
|
39
|
+
success: (_result: AuthResult) => true;
|
|
40
|
+
error: (_error: GlideError) => true;
|
|
41
|
+
phasechange: (_phase: string) => true;
|
|
42
|
+
}, string, vue.PublicProps, Readonly<vue.ExtractPropTypes<{
|
|
43
|
+
mode: {
|
|
44
|
+
type: PropType<AuthMode>;
|
|
45
|
+
default: string;
|
|
46
|
+
};
|
|
47
|
+
username: {
|
|
48
|
+
type: StringConstructor;
|
|
49
|
+
default: undefined;
|
|
50
|
+
};
|
|
51
|
+
label: {
|
|
52
|
+
type: StringConstructor;
|
|
53
|
+
default: undefined;
|
|
54
|
+
};
|
|
55
|
+
endpoints: {
|
|
56
|
+
type: PropType<Partial<GlideEndpoints>>;
|
|
57
|
+
default: undefined;
|
|
58
|
+
};
|
|
59
|
+
fetchOptions: {
|
|
60
|
+
type: PropType<RequestInit>;
|
|
61
|
+
default: undefined;
|
|
62
|
+
};
|
|
63
|
+
}>> & {
|
|
64
|
+
onSuccess?: ((_result: AuthResult) => any) | undefined;
|
|
65
|
+
onError?: ((_error: GlideError) => any) | undefined;
|
|
66
|
+
onPhasechange?: ((_phase: string) => any) | undefined;
|
|
67
|
+
}, {
|
|
68
|
+
mode: AuthMode;
|
|
69
|
+
username: string;
|
|
70
|
+
label: string;
|
|
71
|
+
endpoints: Partial<GlideEndpoints>;
|
|
72
|
+
fetchOptions: RequestInit;
|
|
73
|
+
}, {}>;
|
|
74
|
+
|
|
75
|
+
/** Element instance shape we rely on (subset of BiometricAuthButton). */
|
|
76
|
+
interface GlideElement extends HTMLElement {
|
|
77
|
+
phase: AuthPhase;
|
|
78
|
+
endpoints: Partial<GlideEndpoints>;
|
|
79
|
+
fetchOptions: RequestInit;
|
|
80
|
+
}
|
|
81
|
+
interface UsePasskeyAuthOptions {
|
|
82
|
+
mode?: AuthMode;
|
|
83
|
+
username?: string;
|
|
84
|
+
endpoints?: Partial<GlideEndpoints>;
|
|
85
|
+
/** Forwarded to fetch (e.g. custom headers). credentials:"include" is default. */
|
|
86
|
+
fetchOptions?: RequestInit;
|
|
87
|
+
onSuccess?: (result: AuthResult) => void;
|
|
88
|
+
onError?: (error: GlideError) => void;
|
|
89
|
+
}
|
|
90
|
+
interface UsePasskeyAuth {
|
|
91
|
+
/** Bind to <biometric-auth-button :ref="elRef" /> (or <PasskeyButton :ref> ). */
|
|
92
|
+
elRef: Ref<GlideElement | null>;
|
|
93
|
+
phase: Ref<AuthPhase>;
|
|
94
|
+
/** True while the ceremony is in flight. */
|
|
95
|
+
isPending: ComputedRef<boolean>;
|
|
96
|
+
/** True once a feature-detect failure is reflected by the element. */
|
|
97
|
+
isUnsupported: ComputedRef<boolean>;
|
|
98
|
+
error: Ref<GlideError | null>;
|
|
99
|
+
user: Ref<GlideUser | null>;
|
|
100
|
+
/** Imperatively start auth (same as a user click). */
|
|
101
|
+
trigger: () => void;
|
|
102
|
+
reset: () => void;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Headless composable wrapping the <biometric-auth-button> Web Component.
|
|
106
|
+
*
|
|
107
|
+
* It owns nothing about rendering — you bring the element (via <PasskeyButton>
|
|
108
|
+
* or a raw tag) and bind `elRef`. The composable subscribes to the element's
|
|
109
|
+
* `glide:*` CustomEvents and projects them into reactive refs + callbacks.
|
|
110
|
+
*/
|
|
111
|
+
declare function usePasskeyAuth(options?: UsePasskeyAuthOptions): UsePasskeyAuth;
|
|
112
|
+
|
|
113
|
+
export { PasskeyButton, type UsePasskeyAuth, type UsePasskeyAuthOptions, usePasskeyAuth };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { defineComponent, ref, onMounted, watch, onBeforeUnmount, h, shallowRef, computed } from 'vue';
|
|
2
|
+
|
|
3
|
+
// src/PasskeyButton.ts
|
|
4
|
+
var PasskeyButton = defineComponent({
|
|
5
|
+
name: "PasskeyButton",
|
|
6
|
+
props: {
|
|
7
|
+
mode: {
|
|
8
|
+
type: String,
|
|
9
|
+
default: "auto"
|
|
10
|
+
},
|
|
11
|
+
username: { type: String, default: void 0 },
|
|
12
|
+
label: { type: String, default: void 0 },
|
|
13
|
+
endpoints: {
|
|
14
|
+
type: Object,
|
|
15
|
+
default: void 0
|
|
16
|
+
},
|
|
17
|
+
fetchOptions: {
|
|
18
|
+
type: Object,
|
|
19
|
+
default: void 0
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
emits: {
|
|
23
|
+
success: (_result) => true,
|
|
24
|
+
error: (_error) => true,
|
|
25
|
+
phasechange: (_phase) => true
|
|
26
|
+
},
|
|
27
|
+
setup(props, { emit }) {
|
|
28
|
+
const elRef = ref(null);
|
|
29
|
+
const onSuccess = (e) => emit("success", e.detail);
|
|
30
|
+
const onError = (e) => emit("error", e.detail);
|
|
31
|
+
const onPhase = (e) => emit("phasechange", e.detail.phase);
|
|
32
|
+
onMounted(async () => {
|
|
33
|
+
await import('@glydi/passkey-core/define');
|
|
34
|
+
const el = elRef.value;
|
|
35
|
+
if (!el)
|
|
36
|
+
return;
|
|
37
|
+
if (props.endpoints)
|
|
38
|
+
el.endpoints = props.endpoints;
|
|
39
|
+
if (props.fetchOptions)
|
|
40
|
+
el.fetchOptions = props.fetchOptions;
|
|
41
|
+
el.addEventListener("glide:success", onSuccess);
|
|
42
|
+
el.addEventListener("glide:error", onError);
|
|
43
|
+
el.addEventListener("glide:phasechange", onPhase);
|
|
44
|
+
});
|
|
45
|
+
watch(
|
|
46
|
+
() => props.endpoints,
|
|
47
|
+
(v) => {
|
|
48
|
+
if (elRef.value && v)
|
|
49
|
+
elRef.value.endpoints = v;
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
watch(
|
|
53
|
+
() => props.fetchOptions,
|
|
54
|
+
(v) => {
|
|
55
|
+
if (elRef.value && v)
|
|
56
|
+
elRef.value.fetchOptions = v;
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
onBeforeUnmount(() => {
|
|
60
|
+
const el = elRef.value;
|
|
61
|
+
if (!el)
|
|
62
|
+
return;
|
|
63
|
+
el.removeEventListener("glide:success", onSuccess);
|
|
64
|
+
el.removeEventListener("glide:error", onError);
|
|
65
|
+
el.removeEventListener("glide:phasechange", onPhase);
|
|
66
|
+
});
|
|
67
|
+
return () => h("biometric-auth-button", {
|
|
68
|
+
ref: elRef,
|
|
69
|
+
mode: props.mode,
|
|
70
|
+
username: props.username,
|
|
71
|
+
label: props.label
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
function usePasskeyAuth(options = {}) {
|
|
76
|
+
const elRef = ref(null);
|
|
77
|
+
const phase = ref("idle");
|
|
78
|
+
const error = shallowRef(null);
|
|
79
|
+
const user = shallowRef(null);
|
|
80
|
+
const onSuccess = (e) => {
|
|
81
|
+
const detail = e.detail;
|
|
82
|
+
user.value = detail.user;
|
|
83
|
+
error.value = null;
|
|
84
|
+
options.onSuccess?.(detail);
|
|
85
|
+
};
|
|
86
|
+
const onError = (e) => {
|
|
87
|
+
const detail = e.detail;
|
|
88
|
+
error.value = detail;
|
|
89
|
+
options.onError?.(detail);
|
|
90
|
+
};
|
|
91
|
+
const onPhase = (e) => {
|
|
92
|
+
phase.value = e.detail.phase;
|
|
93
|
+
};
|
|
94
|
+
const attach = (el) => {
|
|
95
|
+
if (options.endpoints)
|
|
96
|
+
el.endpoints = options.endpoints;
|
|
97
|
+
if (options.fetchOptions)
|
|
98
|
+
el.fetchOptions = options.fetchOptions;
|
|
99
|
+
el.addEventListener("glide:success", onSuccess);
|
|
100
|
+
el.addEventListener("glide:error", onError);
|
|
101
|
+
el.addEventListener("glide:phasechange", onPhase);
|
|
102
|
+
phase.value = el.phase;
|
|
103
|
+
};
|
|
104
|
+
const detach = (el) => {
|
|
105
|
+
el.removeEventListener("glide:success", onSuccess);
|
|
106
|
+
el.removeEventListener("glide:error", onError);
|
|
107
|
+
el.removeEventListener("glide:phasechange", onPhase);
|
|
108
|
+
};
|
|
109
|
+
onMounted(async () => {
|
|
110
|
+
await import('@glydi/passkey-core/define');
|
|
111
|
+
if (elRef.value)
|
|
112
|
+
attach(elRef.value);
|
|
113
|
+
});
|
|
114
|
+
watch(elRef, (el, prev) => {
|
|
115
|
+
if (prev)
|
|
116
|
+
detach(prev);
|
|
117
|
+
if (el)
|
|
118
|
+
attach(el);
|
|
119
|
+
});
|
|
120
|
+
onBeforeUnmount(() => {
|
|
121
|
+
if (elRef.value)
|
|
122
|
+
detach(elRef.value);
|
|
123
|
+
});
|
|
124
|
+
const trigger = () => {
|
|
125
|
+
elRef.value?.click();
|
|
126
|
+
};
|
|
127
|
+
const reset = () => {
|
|
128
|
+
error.value = null;
|
|
129
|
+
user.value = null;
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
elRef,
|
|
133
|
+
phase,
|
|
134
|
+
isPending: computed(
|
|
135
|
+
() => phase.value === "loading" || phase.value === "authenticating"
|
|
136
|
+
),
|
|
137
|
+
isUnsupported: computed(() => phase.value === "unsupported"),
|
|
138
|
+
error,
|
|
139
|
+
user,
|
|
140
|
+
trigger,
|
|
141
|
+
reset
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export { PasskeyButton, usePasskeyAuth };
|
|
146
|
+
//# sourceMappingURL=out.js.map
|
|
147
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/PasskeyButton.ts","../src/usePasskeyAuth.ts"],"names":["onMounted","onBeforeUnmount","ref","watch"],"mappings":"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","sourcesContent":["import {\n defineComponent,\n h,\n onMounted,\n onBeforeUnmount,\n ref,\n watch,\n type PropType,\n} from \"vue\";\nimport type {\n AuthMode,\n AuthResult,\n GlideEndpoints,\n GlideError,\n} from \"@glydi/passkey-core\";\n\n/** Element instance shape we rely on (subset of BiometricAuthButton). */\ninterface GlideElement extends HTMLElement {\n endpoints: Partial<GlideEndpoints>;\n fetchOptions: RequestInit;\n}\n\n/**\n * Thin declarative wrapper. This is intentionally tiny: all real work lives in\n * the Web Component. The wrapper only:\n * 1. ensures the element is registered (client-side, in onMounted),\n * 2. forwards object props the DOM can't express as attributes, and\n * 3. bridges CustomEvents → Vue emits.\n *\n * For full state (phase/error/user) use `usePasskeyAuth` instead.\n */\nexport const PasskeyButton = defineComponent({\n name: \"PasskeyButton\",\n props: {\n mode: {\n type: String as PropType<AuthMode>,\n default: \"auto\",\n },\n username: { type: String, default: undefined },\n label: { type: String, default: undefined },\n endpoints: {\n type: Object as PropType<Partial<GlideEndpoints>>,\n default: undefined,\n },\n fetchOptions: {\n type: Object as PropType<RequestInit>,\n default: undefined,\n },\n },\n emits: {\n success: (_result: AuthResult) => true,\n error: (_error: GlideError) => true,\n phasechange: (_phase: string) => true,\n },\n setup(props, { emit }) {\n const elRef = ref<GlideElement | null>(null);\n\n const onSuccess = (e: Event) =>\n emit(\"success\", (e as CustomEvent<AuthResult>).detail);\n const onError = (e: Event) =>\n emit(\"error\", (e as CustomEvent<GlideError>).detail);\n const onPhase = (e: Event) =>\n emit(\"phasechange\", (e as CustomEvent<{ phase: string }>).detail.phase);\n\n onMounted(async () => {\n // Register <biometric-auth-button> on the client only — the core class\n // extends HTMLElement, so importing \"/define\" must not run during SSR.\n await import(\"@glydi/passkey-core/define\");\n\n const el = elRef.value;\n if (!el) return;\n\n // Forward non-serializable props (objects) imperatively.\n if (props.endpoints) el.endpoints = props.endpoints;\n if (props.fetchOptions) el.fetchOptions = props.fetchOptions;\n\n el.addEventListener(\"glide:success\", onSuccess);\n el.addEventListener(\"glide:error\", onError);\n el.addEventListener(\"glide:phasechange\", onPhase);\n });\n\n // Keep imperative object props in sync after mount.\n watch(\n () => props.endpoints,\n (v) => {\n if (elRef.value && v) elRef.value.endpoints = v;\n },\n );\n watch(\n () => props.fetchOptions,\n (v) => {\n if (elRef.value && v) elRef.value.fetchOptions = v;\n },\n );\n\n onBeforeUnmount(() => {\n const el = elRef.value;\n if (!el) return;\n el.removeEventListener(\"glide:success\", onSuccess);\n el.removeEventListener(\"glide:error\", onError);\n el.removeEventListener(\"glide:phasechange\", onPhase);\n });\n\n return () =>\n h(\"biometric-auth-button\", {\n ref: elRef,\n mode: props.mode,\n username: props.username,\n label: props.label,\n });\n },\n});\n","import {\n computed,\n onMounted,\n onBeforeUnmount,\n ref,\n shallowRef,\n watch,\n type ComputedRef,\n type Ref,\n} from \"vue\";\nimport type {\n AuthMode,\n AuthPhase,\n AuthResult,\n GlideEndpoints,\n GlideError,\n GlideUser,\n} from \"@glydi/passkey-core\";\n\n/** Element instance shape we rely on (subset of BiometricAuthButton). */\ninterface GlideElement extends HTMLElement {\n phase: AuthPhase;\n endpoints: Partial<GlideEndpoints>;\n fetchOptions: RequestInit;\n}\n\nexport interface UsePasskeyAuthOptions {\n mode?: AuthMode;\n username?: string;\n endpoints?: Partial<GlideEndpoints>;\n /** Forwarded to fetch (e.g. custom headers). credentials:\"include\" is default. */\n fetchOptions?: RequestInit;\n onSuccess?: (result: AuthResult) => void;\n onError?: (error: GlideError) => void;\n}\n\nexport interface UsePasskeyAuth {\n /** Bind to <biometric-auth-button :ref=\"elRef\" /> (or <PasskeyButton :ref> ). */\n elRef: Ref<GlideElement | null>;\n phase: Ref<AuthPhase>;\n /** True while the ceremony is in flight. */\n isPending: ComputedRef<boolean>;\n /** True once a feature-detect failure is reflected by the element. */\n isUnsupported: ComputedRef<boolean>;\n error: Ref<GlideError | null>;\n user: Ref<GlideUser | null>;\n /** Imperatively start auth (same as a user click). */\n trigger: () => void;\n reset: () => void;\n}\n\n/**\n * Headless composable wrapping the <biometric-auth-button> Web Component.\n *\n * It owns nothing about rendering — you bring the element (via <PasskeyButton>\n * or a raw tag) and bind `elRef`. The composable subscribes to the element's\n * `glide:*` CustomEvents and projects them into reactive refs + callbacks.\n */\nexport function usePasskeyAuth(\n options: UsePasskeyAuthOptions = {},\n): UsePasskeyAuth {\n const elRef = ref<GlideElement | null>(null);\n const phase = ref<AuthPhase>(\"idle\");\n const error = shallowRef<GlideError | null>(null);\n const user = shallowRef<GlideUser | null>(null);\n\n const onSuccess = (e: Event) => {\n const detail = (e as CustomEvent<AuthResult>).detail;\n user.value = detail.user;\n error.value = null;\n options.onSuccess?.(detail);\n };\n const onError = (e: Event) => {\n const detail = (e as CustomEvent<GlideError>).detail;\n error.value = detail;\n options.onError?.(detail);\n };\n const onPhase = (e: Event) => {\n phase.value = (e as CustomEvent<{ phase: AuthPhase }>).detail.phase;\n };\n\n const attach = (el: GlideElement) => {\n // Push imperative config (objects can't be set as attributes).\n if (options.endpoints) el.endpoints = options.endpoints;\n if (options.fetchOptions) el.fetchOptions = options.fetchOptions;\n\n el.addEventListener(\"glide:success\", onSuccess);\n el.addEventListener(\"glide:error\", onError);\n el.addEventListener(\"glide:phasechange\", onPhase);\n // Sync any phase already reflected before listeners attached.\n phase.value = el.phase;\n };\n\n const detach = (el: GlideElement) => {\n el.removeEventListener(\"glide:success\", onSuccess);\n el.removeEventListener(\"glide:error\", onError);\n el.removeEventListener(\"glide:phasechange\", onPhase);\n };\n\n onMounted(async () => {\n // Register the element on the client only (the core extends HTMLElement).\n await import(\"@glydi/passkey-core/define\");\n if (elRef.value) attach(elRef.value);\n });\n\n // Re-bind if the underlying element instance changes.\n watch(elRef, (el, prev) => {\n if (prev) detach(prev);\n if (el) attach(el);\n });\n\n onBeforeUnmount(() => {\n if (elRef.value) detach(elRef.value);\n });\n\n const trigger = () => {\n elRef.value?.click();\n };\n\n const reset = () => {\n error.value = null;\n user.value = null;\n };\n\n return {\n elRef,\n phase,\n isPending: computed(\n () => phase.value === \"loading\" || phase.value === \"authenticating\",\n ),\n isUnsupported: computed(() => phase.value === \"unsupported\"),\n error,\n user,\n trigger,\n reset,\n };\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@glydi/passkey-vue",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "Thin Vue 3 adapter for the Glide passkey Web Component.",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"module": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"vue": ">=3.3"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@glydi/passkey-core": "0.1.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"tsup": "^8.0.0",
|
|
29
|
+
"typescript": "^5.4.0",
|
|
30
|
+
"vue": "^3.4.0"
|
|
31
|
+
},
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsup",
|
|
35
|
+
"dev": "tsup --watch",
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"clean": "rm -rf dist .turbo"
|
|
38
|
+
}
|
|
39
|
+
}
|