@synqauth/plugin-passkey 0.0.0-stage → 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aerosol
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,28 @@
1
- # Temporary Holding Version
1
+ # @synqauth/plugin-passkey
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A Synq plugin for passkeys from your own UI.
4
+
5
+ Synq is customer identity for businesses: an OpenID Connect issuer for each of your brands, with Solana wallet, social, email and passkey sign-in. The documentation is at [docs.synq.build](https://docs.synq.build).
6
+
7
+ This package is at 0.x and hasn't been published yet: its API can change in any release until Synq's first customer depends on it.
8
+
9
+ ## Usage
10
+
11
+ Passkeys from your own UI, with every ceremony at your brand's Synq issuer, which verifies them. Nothing is verified on your server.
12
+
13
+ ```tsx
14
+ import { passkeyPlugin } from '@synqauth/plugin-passkey'
15
+ import { SynqProvider, usePlugin } from '@synqauth/react'
16
+
17
+ const passkey = passkeyPlugin()
18
+
19
+ // <SynqProvider plugins={[passkey]}> ... then, in a component:
20
+ const { signInUrl, addUrl, list, remove } = usePlugin(passkey)
21
+ location.assign(signInUrl({ returnTo: '/account' }))
22
+ ```
23
+
24
+ `list()` and `remove(id)` go through your `@synqauth/nextjs` route mount's account API and answer `{ data, error }`.
25
+
26
+ ## License
27
+
28
+ MIT
@@ -0,0 +1,50 @@
1
+ import { AccountComponents, SynqError } from "@synqauth/js";
2
+ //#region src/index.d.ts
3
+ /** The plugin's id: its actions are under it in `usePlugin` and the mount's plugin list. */
4
+ export declare const PASSKEY_PLUGIN_ID = "passkey";
5
+ /** A passkey of the signed-in person's, as the account API lists it. */
6
+ export type Passkey = AccountComponents['schemas']['Passkey'];
7
+ /** A call's outcome: its data, or the `SynqError` it failed with. */
8
+ export type PasskeyResult<T> = {
9
+ readonly data: T;
10
+ readonly error: null;
11
+ } | {
12
+ readonly data: null;
13
+ readonly error: SynqError;
14
+ };
15
+ /** Calls the app's route mount, from its own page: what @synqauth/react and @synqauth/nextjs/client hand a plugin. */
16
+ export type MountRequest = (path: string, init?: RequestInit) => Promise<Response>;
17
+ /** What the passkey plugin does from the page: where to send the person, and their passkeys. */
18
+ export interface PasskeyActions {
19
+ /** Where to send someone to sign in with a passkey: the brand's page, at its passkey step. */
20
+ signInUrl: (options?: {
21
+ returnTo?: string;
22
+ loginHint?: string;
23
+ }) => string;
24
+ /**
25
+ * Where to send the signed-in person to add a passkey: a fresh sign-in,
26
+ * then the brand's page offers a new passkey (`synq_intent=add-passkey`).
27
+ */
28
+ addUrl: (options?: {
29
+ returnTo?: string;
30
+ }) => string;
31
+ /** Their passkeys. */
32
+ list: () => Promise<PasskeyResult<readonly Passkey[]>>;
33
+ /** Removes one; a passkey that is their last way to sign in can't go (`last_sign_in_method`). */
34
+ remove: (id: string) => Promise<PasskeyResult<null>>;
35
+ }
36
+ /** The passkey plugin, in `SynqClientPlugin`'s shape: for `<SynqProvider plugins>` or `@synqauth/nextjs/client`. */
37
+ export interface PasskeyPlugin {
38
+ readonly id: typeof PASSKEY_PLUGIN_ID;
39
+ actions: (request: MountRequest) => PasskeyActions;
40
+ }
41
+ /** What `passkeyPlugin()` takes. */
42
+ export interface PasskeyPluginOptions {
43
+ /** The route mount's path, as the app's `synqAuth()` has it: `/api/auth` by default. */
44
+ basePath?: string;
45
+ }
46
+ /** The passkey plugin: sign-in and adding a passkey at the brand's issuer, and the person's passkeys through the route mount. */
47
+ export declare function passkeyPlugin(options?: PasskeyPluginOptions): PasskeyPlugin;
48
+ /** Calls the mount and reads its JSON; a problem it answers, or no answer, is the result's `error`. */
49
+ export declare function call<T>(request: MountRequest, path: string, init?: RequestInit): Promise<PasskeyResult<T>>;
50
+ //#endregion
package/dist/index.js ADDED
@@ -0,0 +1,80 @@
1
+ import { SynqError, problemError } from "@synqauth/js";
2
+ //#region src/index.ts
3
+ /** The plugin's id: its actions are under it in `usePlugin` and the mount's plugin list. */
4
+ const PASSKEY_PLUGIN_ID = "passkey";
5
+ /** The passkey plugin: sign-in and adding a passkey at the brand's issuer, and the person's passkeys through the route mount. */
6
+ function passkeyPlugin(options = {}) {
7
+ const base = `/${(options.basePath ?? "/api/auth").replace(/^\/+|\/+$/g, "")}`;
8
+ const signInPath = (params) => {
9
+ const query = new URLSearchParams();
10
+ for (const [name, value] of Object.entries(params)) if (value) query.set(name, value);
11
+ return `${base}/sign-in?${query.toString()}`;
12
+ };
13
+ return {
14
+ id: PASSKEY_PLUGIN_ID,
15
+ actions: (request) => ({
16
+ signInUrl: ({ returnTo, loginHint } = {}) => signInPath({
17
+ method: "passkey",
18
+ returnTo,
19
+ login_hint: loginHint
20
+ }),
21
+ addUrl: ({ returnTo } = {}) => signInPath({
22
+ intent: "add-passkey",
23
+ returnTo
24
+ }),
25
+ list: async () => {
26
+ const result = await call(request, "account/v1/passkeys");
27
+ if (result.error) return {
28
+ data: null,
29
+ error: result.error
30
+ };
31
+ return {
32
+ data: result.data.data,
33
+ error: null
34
+ };
35
+ },
36
+ remove: (id) => {
37
+ if (!id || /^\.+$/.test(id)) return Promise.resolve({
38
+ data: null,
39
+ error: new SynqError("invalid_options", "A passkey id is needed")
40
+ });
41
+ return call(request, `account/v1/passkeys/${encodeURIComponent(id)}`, {
42
+ method: "DELETE",
43
+ headers: { "idempotency-key": crypto.randomUUID() }
44
+ });
45
+ }
46
+ })
47
+ };
48
+ }
49
+ /** Calls the mount and reads its JSON; a problem it answers, or no answer, is the result's `error`. */
50
+ async function call(request, path, init) {
51
+ let response;
52
+ try {
53
+ response = await request(path, init);
54
+ } catch (error) {
55
+ return {
56
+ data: null,
57
+ error: new SynqError("network_error", `The app couldn't be reached: ${String(error)}`, { cause: error })
58
+ };
59
+ }
60
+ const text = await response.text();
61
+ let json;
62
+ try {
63
+ json = text ? JSON.parse(text) : void 0;
64
+ } catch {
65
+ json = void 0;
66
+ }
67
+ if (!response.ok) {
68
+ const problem = json !== null && typeof json === "object" ? json : {};
69
+ return {
70
+ data: null,
71
+ error: problemError(response.status, problem)
72
+ };
73
+ }
74
+ return {
75
+ data: json ?? null,
76
+ error: null
77
+ };
78
+ }
79
+ //#endregion
80
+ export { PASSKEY_PLUGIN_ID, call, passkeyPlugin };
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "@synqauth/plugin-passkey",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.0",
4
+ "description": "A Synq plugin for passkeys from your own UI.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/aerosol-build/synq.git",
9
+ "directory": "packages/plugin-passkey"
10
+ },
11
+ "type": "module",
12
+ "sideEffects": false,
13
+ "engines": {
14
+ "node": ">=22"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "dependencies": {
30
+ "@synqauth/js": "0.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "vitest": "5.0.2"
34
+ },
35
+ "scripts": {
36
+ "build": "tsdown",
37
+ "lint": "eslint .",
38
+ "typecheck": "tsc",
39
+ "test": "vitest run"
40
+ }
6
41
  }