@openape/nuxt-auth-sp 0.1.10 → 0.3.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/dist/module.d.mts CHANGED
@@ -6,6 +6,7 @@ interface ModuleOptions {
6
6
  sessionSecret: string;
7
7
  openapeUrl: string;
8
8
  fallbackIdpUrl: string;
9
+ routes: boolean;
9
10
  }
10
11
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
11
12
 
package/dist/module.d.ts CHANGED
@@ -6,6 +6,7 @@ interface ModuleOptions {
6
6
  sessionSecret: string;
7
7
  openapeUrl: string;
8
8
  fallbackIdpUrl: string;
9
+ routes: boolean;
9
10
  }
10
11
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
11
12
 
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@openape/nuxt-auth-sp",
3
3
  "configKey": "openapeSp",
4
- "version": "0.1.10",
4
+ "version": "0.3.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "0.8.4",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -1,6 +1,8 @@
1
- import { defineNuxtModule, createResolver, addServerImportsDir, addImportsDir, addServerHandler } from '@nuxt/kit';
1
+ import crypto from 'node:crypto';
2
+ import { useLogger, defineNuxtModule, createResolver, addServerImportsDir, addImportsDir, addComponentsDir, addServerHandler } from '@nuxt/kit';
2
3
  import { defu } from 'defu';
3
4
 
5
+ const logger = useLogger("@openape/nuxt-auth-sp");
4
6
  const module = defineNuxtModule({
5
7
  meta: {
6
8
  name: "@openape/nuxt-auth-sp",
@@ -11,21 +13,48 @@ const module = defineNuxtModule({
11
13
  spName: "OpenApe Service Provider",
12
14
  sessionSecret: "change-me-sp-secret-at-least-32-chars-long",
13
15
  openapeUrl: "",
14
- fallbackIdpUrl: "https://id.openape.at"
16
+ fallbackIdpUrl: "https://id.openape.at",
17
+ routes: true
15
18
  },
16
19
  setup(options, nuxt) {
17
20
  const { resolve } = createResolver(import.meta.url);
21
+ const runtimeDir = resolve("./runtime");
18
22
  nuxt.options.runtimeConfig.openapeSp = defu(
19
23
  nuxt.options.runtimeConfig.openapeSp || {},
20
24
  options
21
25
  );
26
+ if (nuxt.options.dev) {
27
+ const config = nuxt.options.runtimeConfig.openapeSp;
28
+ if (!config.sessionSecret || config.sessionSecret === "change-me-sp-secret-at-least-32-chars-long") {
29
+ config.sessionSecret = crypto.randomUUID() + crypto.randomUUID();
30
+ logger.info("Auto-generated sessionSecret for dev mode");
31
+ }
32
+ if (!config.spId) {
33
+ const port = nuxt.options.devServer?.port || 3e3;
34
+ config.spId = `localhost:${port}`;
35
+ logger.info(`Auto-derived spId: ${config.spId}`);
36
+ }
37
+ }
38
+ if (!nuxt.options.dev) {
39
+ const config = nuxt.options.runtimeConfig.openapeSp;
40
+ if (config.sessionSecret === "change-me-sp-secret-at-least-32-chars-long") {
41
+ logger.warn("Using default sessionSecret in production! Set NUXT_OPENAPE_SP_SESSION_SECRET.");
42
+ }
43
+ if (!config.spId) {
44
+ logger.warn("spId is empty in production! Set openapeSp.spId or NUXT_OPENAPE_SP_SP_ID.");
45
+ }
46
+ }
22
47
  addServerImportsDir(resolve("./runtime/server/utils"));
23
48
  addImportsDir(resolve("./runtime/composables"));
24
- addServerHandler({ route: "/api/login", method: "post", handler: resolve("./runtime/server/api/login.post") });
25
- addServerHandler({ route: "/api/callback", handler: resolve("./runtime/server/api/callback.get") });
26
- addServerHandler({ route: "/api/logout", method: "post", handler: resolve("./runtime/server/api/logout.post") });
27
- addServerHandler({ route: "/api/me", handler: resolve("./runtime/server/api/me.get") });
28
- addServerHandler({ route: "/.well-known/sp-manifest.json", handler: resolve("./runtime/server/routes/well-known/sp-manifest.json.get") });
49
+ addComponentsDir({ path: resolve(runtimeDir, "components") });
50
+ if (options.routes !== false) {
51
+ addServerHandler({ route: "/api/login", method: "post", handler: resolve("./runtime/server/api/login.post") });
52
+ addServerHandler({ route: "/api/callback", handler: resolve("./runtime/server/api/callback.get") });
53
+ addServerHandler({ route: "/api/logout", method: "post", handler: resolve("./runtime/server/api/logout.post") });
54
+ addServerHandler({ route: "/api/me", handler: resolve("./runtime/server/api/me.get") });
55
+ addServerHandler({ route: "/.well-known/sp-manifest.json", handler: resolve("./runtime/server/routes/well-known/sp-manifest.json.get") });
56
+ addServerHandler({ route: "/.well-known/auth.md", handler: resolve("./runtime/server/routes/well-known/auth.md.get") });
57
+ }
29
58
  }
30
59
  });
31
60
 
@@ -0,0 +1,112 @@
1
+ <script setup lang="ts">
2
+ withDefaults(defineProps<{
3
+ title?: string
4
+ subtitle?: string
5
+ buttonText?: string
6
+ placeholder?: string
7
+ }>(), {
8
+ title: 'Sign in',
9
+ subtitle: 'Enter your email to continue',
10
+ buttonText: 'Continue',
11
+ placeholder: 'you@example.com',
12
+ })
13
+
14
+ const emit = defineEmits<{
15
+ error: [error: Error]
16
+ }>()
17
+
18
+ const { user, loading, fetchUser, login } = useOpenApeAuth()
19
+ const email = ref('')
20
+ const error = ref('')
21
+ const submitting = ref(false)
22
+
23
+ const route = useRoute()
24
+
25
+ onMounted(async () => {
26
+ await fetchUser()
27
+ if (user.value) {
28
+ navigateTo('/dashboard')
29
+ }
30
+ if (route.query.error) {
31
+ error.value = String(route.query.error)
32
+ }
33
+ })
34
+
35
+ async function handleSubmit() {
36
+ error.value = ''
37
+ if (!email.value || !email.value.includes('@')) {
38
+ error.value = 'Please enter a valid email address'
39
+ return
40
+ }
41
+ submitting.value = true
42
+ try {
43
+ await login(email.value)
44
+ }
45
+ catch (e: unknown) {
46
+ const err = e instanceof Error ? e : new Error('Login failed')
47
+ error.value = (e as any)?.data?.message || err.message
48
+ emit('error', err)
49
+ submitting.value = false
50
+ }
51
+ }
52
+ </script>
53
+
54
+ <template>
55
+ <div v-if="loading" class="openape-auth openape-auth--loading">
56
+ <div class="openape-auth-spinner" />
57
+ </div>
58
+
59
+ <div v-else class="openape-auth">
60
+ <slot name="header">
61
+ <div class="openape-auth-header">
62
+ <h2 class="openape-auth-title">
63
+ {{ title }}
64
+ </h2>
65
+ <p class="openape-auth-subtitle">
66
+ {{ subtitle }}
67
+ </p>
68
+ </div>
69
+ </slot>
70
+
71
+ <form class="openape-auth-form" @submit.prevent="handleSubmit">
72
+ <slot name="error" :error="error">
73
+ <p v-if="error" class="openape-auth-error">
74
+ {{ error }}
75
+ </p>
76
+ </slot>
77
+
78
+ <input
79
+ v-model="email"
80
+ type="email"
81
+ class="openape-auth-input"
82
+ :placeholder="placeholder"
83
+ required
84
+ :disabled="submitting"
85
+ autocomplete="email"
86
+ >
87
+
88
+ <slot name="button" :submitting="submitting">
89
+ <button
90
+ type="submit"
91
+ class="openape-auth-button"
92
+ :disabled="submitting || !email"
93
+ >
94
+ <span v-if="submitting" class="openape-auth-button-loading">
95
+ <svg class="openape-auth-spinner-icon" viewBox="0 0 24 24" fill="none">
96
+ <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2.5" opacity="0.25" />
97
+ <path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" />
98
+ </svg>
99
+ Redirecting…
100
+ </span>
101
+ <span v-else>{{ buttonText }}</span>
102
+ </button>
103
+ </slot>
104
+ </form>
105
+
106
+ <slot name="footer" />
107
+ </div>
108
+ </template>
109
+
110
+ <style>
111
+ .openape-auth{--oa-bg:#fff;--oa-border:#e2e2e2;--oa-text:#1a1a1a;--oa-text-muted:#6b7280;--oa-primary:#18181b;--oa-primary-hover:#27272a;--oa-primary-text:#fff;--oa-error:#dc2626;--oa-error-bg:#fef2f2;--oa-input-bg:#fff;--oa-input-border:#d1d5db;--oa-input-focus:#18181b;--oa-radius:8px;--oa-font:system-ui,-apple-system,sans-serif;background:var(--oa-bg);border:1px solid var(--oa-border);border-radius:var(--oa-radius);box-sizing:border-box;color:var(--oa-text);font-family:var(--oa-font);max-width:400px;padding:2rem;width:100%}.openape-auth--loading{align-items:center;display:flex;justify-content:center;min-height:200px}.openape-auth-spinner{animation:oa-spin .6s linear infinite;border:2.5px solid var(--oa-border);border-radius:50%;border-top-color:var(--oa-primary);height:24px;width:24px}.openape-auth-header{margin-bottom:1.5rem;text-align:center}.openape-auth-title{color:var(--oa-text);font-size:1.375rem;font-weight:600;letter-spacing:-.01em;margin:0 0 .375rem}.openape-auth-subtitle{color:var(--oa-text-muted);font-size:.875rem;margin:0}.openape-auth-form{display:flex;flex-direction:column;gap:.75rem}.openape-auth-error{background:var(--oa-error-bg);border-radius:calc(var(--oa-radius) - 2px);color:var(--oa-error);font-size:.8125rem;margin:0;padding:.625rem .75rem}.openape-auth-input{background:var(--oa-input-bg);border:1px solid var(--oa-input-border);border-radius:calc(var(--oa-radius) - 2px);box-sizing:border-box;color:var(--oa-text);font-family:var(--oa-font);font-size:.9375rem;outline:none;padding:.625rem .75rem;transition:border-color .15s;width:100%}.openape-auth-input:focus{border-color:var(--oa-input-focus);box-shadow:0 0 0 1px var(--oa-input-focus)}.openape-auth-input:disabled{cursor:not-allowed;opacity:.6}.openape-auth-button{background:var(--oa-primary);border:none;border-radius:calc(var(--oa-radius) - 2px);color:var(--oa-primary-text);cursor:pointer;font-family:var(--oa-font);font-size:.9375rem;font-weight:500;padding:.625rem 1rem;transition:background .15s}.openape-auth-button:hover:not(:disabled){background:var(--oa-primary-hover)}.openape-auth-button:disabled{cursor:not-allowed;opacity:.5}.openape-auth-button-loading{align-items:center;display:inline-flex;gap:.5rem;justify-content:center}.openape-auth-spinner-icon{animation:oa-spin .6s linear infinite;height:16px;width:16px}@keyframes oa-spin{to{transform:rotate(1turn)}}@media (prefers-color-scheme:dark){.openape-auth{--oa-bg:#18181b;--oa-border:#2e2e32;--oa-text:#f4f4f5;--oa-text-muted:#a1a1aa;--oa-primary:#f4f4f5;--oa-primary-hover:#e4e4e7;--oa-primary-text:#18181b;--oa-error-bg:#2d1215;--oa-input-bg:#1f1f23;--oa-input-border:#3f3f46;--oa-input-focus:#a1a1aa}}
112
+ </style>
@@ -0,0 +1,21 @@
1
+ import type { H3Event } from 'h3';
2
+ import type { DDISAAssertionClaims } from '@openape/core';
3
+ export interface LoginHandlerOptions {
4
+ callbackPath: string;
5
+ }
6
+ export interface CallbackHandlerOptions {
7
+ onSuccess: (event: H3Event, result: {
8
+ claims: DDISAAssertionClaims;
9
+ rawAssertion: string;
10
+ }) => Promise<void>;
11
+ onError?: (event: H3Event, error: Error) => Promise<void>;
12
+ }
13
+ export interface SPManifestHandlerOptions {
14
+ callbackPath: string;
15
+ description?: string;
16
+ }
17
+ export declare function defineOpenApeLoginHandler(options: LoginHandlerOptions): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
18
+ redirectUrl: string;
19
+ }>>;
20
+ export declare function defineOpenApeCallbackHandler(options: CallbackHandlerOptions): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
21
+ export declare function defineOpenApeSPManifestHandler(options: SPManifestHandlerOptions): import("h3").EventHandler<import("h3").EventHandlerRequest, import("@openape/core").SPManifest>;
@@ -0,0 +1,100 @@
1
+ import { createError, defineEventHandler, getQuery, getRequestURL, readBody, sendRedirect } from "h3";
2
+ import { createAuthorizationURL, createSPManifest, discoverIdP, handleCallback } from "@openape/auth";
3
+ import { getSpConfig, saveFlowState, getFlowState, clearFlowState } from "./utils/sp-config.js";
4
+ export function defineOpenApeLoginHandler(options) {
5
+ return defineEventHandler(async (event) => {
6
+ const body = await readBody(event);
7
+ const { spId, openapeUrl, fallbackIdpUrl } = getSpConfig();
8
+ const origin = getRequestURL(event).origin;
9
+ const redirectUri = `${origin}${options.callbackPath}`;
10
+ if (!body?.email || !body.email.includes("@")) {
11
+ throw createError({ statusCode: 400, statusMessage: "Valid email required" });
12
+ }
13
+ const email = body.email.trim();
14
+ const domain = email.split("@")[1];
15
+ let idpConfig;
16
+ if (openapeUrl) {
17
+ idpConfig = { idpUrl: openapeUrl, record: { version: "ddisa1", idp: openapeUrl, raw: `v=ddisa1; idp=${openapeUrl}` } };
18
+ } else {
19
+ idpConfig = await discoverIdP(email, { fallbackIdpUrl: fallbackIdpUrl || void 0 });
20
+ }
21
+ if (!idpConfig) {
22
+ throw createError({
23
+ statusCode: 404,
24
+ statusMessage: `No DDISA IdP found for domain "${domain}"`
25
+ });
26
+ }
27
+ const { url, flowState } = await createAuthorizationURL(idpConfig, {
28
+ spId,
29
+ redirectUri,
30
+ email
31
+ });
32
+ await saveFlowState(event, flowState.state, flowState);
33
+ return { redirectUrl: url };
34
+ });
35
+ }
36
+ export function defineOpenApeCallbackHandler(options) {
37
+ return defineEventHandler(async (event) => {
38
+ const query = getQuery(event);
39
+ const { code, state, error, error_description } = query;
40
+ const { spId } = getSpConfig();
41
+ const origin = getRequestURL(event).origin;
42
+ if (error) {
43
+ const msg = error_description || error;
44
+ if (options.onError) {
45
+ await options.onError(event, new Error(msg));
46
+ return;
47
+ }
48
+ return sendRedirect(event, `/login?error=${encodeURIComponent(msg)}`);
49
+ }
50
+ if (!code || !state) {
51
+ const err = new Error("Missing code or state parameter");
52
+ if (options.onError) {
53
+ await options.onError(event, err);
54
+ return;
55
+ }
56
+ return sendRedirect(event, `/login?error=${encodeURIComponent(err.message)}`);
57
+ }
58
+ const flowState = await getFlowState(event, state);
59
+ if (!flowState) {
60
+ const err = new Error("Invalid or expired state \u2014 please try again");
61
+ if (options.onError) {
62
+ await options.onError(event, err);
63
+ return;
64
+ }
65
+ return sendRedirect(event, `/login?error=${encodeURIComponent(err.message)}`);
66
+ }
67
+ try {
68
+ const redirectUri = `${origin}${getRequestURL(event).pathname}`;
69
+ const result = await handleCallback({
70
+ code,
71
+ state,
72
+ flowState,
73
+ spId,
74
+ redirectUri
75
+ });
76
+ await clearFlowState(event);
77
+ await options.onSuccess(event, { claims: result.claims, rawAssertion: result.rawAssertion });
78
+ } catch (err) {
79
+ await clearFlowState(event);
80
+ const error2 = err instanceof Error ? err : new Error("Callback processing failed");
81
+ if (options.onError) {
82
+ await options.onError(event, error2);
83
+ return;
84
+ }
85
+ return sendRedirect(event, `/login?error=${encodeURIComponent(error2.message)}`);
86
+ }
87
+ });
88
+ }
89
+ export function defineOpenApeSPManifestHandler(options) {
90
+ return defineEventHandler((event) => {
91
+ const { spId, spName } = getSpConfig();
92
+ const origin = getRequestURL(event).origin;
93
+ return createSPManifest({
94
+ sp_id: spId,
95
+ name: spName,
96
+ redirect_uris: [`${origin}${options.callbackPath}`],
97
+ description: options.description || `${spName} \u2014 OpenApe Service Provider`
98
+ });
99
+ });
100
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, string>;
2
+ export default _default;
@@ -0,0 +1,55 @@
1
+ import { defineEventHandler, getRequestURL, setResponseHeader } from "h3";
2
+ import { getSpConfig } from "../../utils/sp-config.js";
3
+ export default defineEventHandler((event) => {
4
+ const { spId, spName, fallbackIdpUrl } = getSpConfig();
5
+ const origin = getRequestURL(event).origin;
6
+ setResponseHeader(event, "Content-Type", "text/markdown; charset=utf-8");
7
+ return `# Authentication \u2014 ${spName}
8
+
9
+ ## Protocol
10
+ DDISA v1 (DNS-Discoverable Identity & Service Authorization)
11
+
12
+ ## Service Provider
13
+ - **SP ID:** \`${spId}\`
14
+ - **Origin:** \`${origin}\`
15
+
16
+ ## Endpoints
17
+
18
+ | Method | Path | Description |
19
+ |--------|------|-------------|
20
+ | POST | /api/login | Start login \u2014 send \`{"email": "you@example.com"}\` |
21
+ | GET | /api/callback | OAuth callback (automatic) |
22
+ | GET | /api/me | Get current session (returns 401 if not authenticated) |
23
+ | POST | /api/logout | End session |
24
+ | GET | /.well-known/sp-manifest.json | SP metadata |
25
+
26
+ ## How to Authenticate
27
+
28
+ ### Step 1 \u2014 Start Login
29
+ \`\`\`
30
+ POST ${origin}/api/login
31
+ Content-Type: application/json
32
+
33
+ {"email": "your-identity@example.com"}
34
+ \`\`\`
35
+ Response: \`{"redirectUrl": "https://idp.example.com/authorize?..."}\`
36
+
37
+ ### Step 2 \u2014 Authenticate at IdP
38
+ Follow the \`redirectUrl\`. The IdP at your email domain (discovered via DNS \`_ddisa.{domain}\`) handles authentication.
39
+
40
+ - **Humans:** WebAuthn passkey prompt
41
+ - **Agents:** Present your Bearer token (obtained via IdP challenge-response)
42
+
43
+ If no DNS record exists for your domain, the fallback IdP \`${fallbackIdpUrl}\` is used.
44
+
45
+ ### Step 3 \u2014 Session Established
46
+ After successful authentication, you are redirected to \`/api/callback\`. A session cookie is set. Use \`GET /api/me\` to verify your session.
47
+
48
+ ## Identity Discovery
49
+ This SP discovers your IdP via DNS TXT record:
50
+ \`\`\`
51
+ _ddisa.{your-domain} TXT "v=ddisa1 idp=https://your-idp.example.com"
52
+ \`\`\`
53
+ No DNS record? The fallback IdP (${fallbackIdpUrl}) is used automatically.
54
+ `;
55
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@openape/nuxt-auth-sp",
3
3
  "type": "module",
4
- "version": "0.1.10",
4
+ "version": "0.3.0",
5
5
  "description": "OpenAPE Service Provider Nuxt module — adds OIDC login via DNS-based IdP discovery",
6
6
  "author": "Delta Mind GmbH",
7
7
  "license": "AGPL-3.0-or-later",
@@ -9,6 +9,10 @@
9
9
  ".": {
10
10
  "types": "./dist/types.d.mts",
11
11
  "import": "./dist/module.mjs"
12
+ },
13
+ "./handlers": {
14
+ "types": "./dist/runtime/server/handlers.d.ts",
15
+ "import": "./dist/runtime/server/handlers.js"
12
16
  }
13
17
  },
14
18
  "main": "./dist/module.mjs",
@@ -29,9 +33,9 @@
29
33
  },
30
34
  "dependencies": {
31
35
  "@nuxt/kit": "^3.21.1",
32
- "@openape/auth": "^0.1.2",
36
+ "@openape/auth": "^0.1.3",
33
37
  "@openape/core": "^0.1.2",
34
- "@openape/nuxt-auth-sp": "0.1.10",
38
+ "@openape/nuxt-auth-sp": "0.3.0",
35
39
  "defu": "^6.1.4"
36
40
  },
37
41
  "peerDependencies": {