@thecodeorigin/auth 0.0.10 → 0.0.11

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.
Files changed (46) hide show
  1. package/README.md +137 -72
  2. package/dist/contract/index.d.mts +80 -3
  3. package/dist/contract/index.d.ts +80 -3
  4. package/dist/contract/index.mjs +34 -2
  5. package/dist/module.d.mts +4 -0
  6. package/dist/module.d.ts +4 -0
  7. package/dist/module.json +1 -1
  8. package/dist/module.mjs +33 -12
  9. package/dist/runtime/app/composables/useAuth.d.ts +5 -0
  10. package/dist/runtime/app/composables/useAuth.js +50 -10
  11. package/dist/runtime/app/composables/useAuthLogin.d.ts +11 -0
  12. package/dist/runtime/app/composables/useAuthLogin.js +73 -0
  13. package/dist/runtime/app/middleware/auth.global.js +2 -0
  14. package/dist/runtime/app/plugins/session-sync.client.d.ts +2 -0
  15. package/dist/runtime/app/plugins/session-sync.client.js +47 -0
  16. package/dist/runtime/app/utils/return-path.d.ts +1 -0
  17. package/dist/runtime/app/utils/return-path.js +18 -0
  18. package/dist/runtime/app/utils/session-recovery.d.ts +5 -0
  19. package/dist/runtime/app/utils/session-recovery.js +23 -0
  20. package/dist/runtime/server/api/_auth/impersonate.post.d.ts +1 -0
  21. package/dist/runtime/server/api/_auth/impersonate.post.js +47 -27
  22. package/dist/runtime/server/api/_auth/organizations/switch.post.d.ts +1 -0
  23. package/dist/runtime/server/api/_auth/organizations/switch.post.js +12 -17
  24. package/dist/runtime/server/api/_auth/session-restore.get.d.ts +5 -0
  25. package/dist/runtime/server/api/_auth/session-restore.get.js +6 -0
  26. package/dist/runtime/server/api/_auth/session.get.d.ts +1 -0
  27. package/dist/runtime/server/api/_auth/sign-out.post.js +3 -0
  28. package/dist/runtime/server/api/_auth/stop-impersonating.post.d.ts +1 -0
  29. package/dist/runtime/server/api/_auth/stop-impersonating.post.js +18 -16
  30. package/dist/runtime/server/index.d.ts +2 -2
  31. package/dist/runtime/server/index.js +1 -1
  32. package/dist/runtime/server/routes/callback.get.js +62 -32
  33. package/dist/runtime/server/routes/sign-in.get.js +8 -6
  34. package/dist/runtime/server/routes/sign-out.get.d.ts +1 -0
  35. package/dist/runtime/server/routes/sign-out.get.js +4 -8
  36. package/dist/runtime/server/utils/idp.d.ts +2 -7
  37. package/dist/runtime/server/utils/idp.js +16 -64
  38. package/dist/runtime/server/utils/integration-key.d.ts +18 -0
  39. package/dist/runtime/server/utils/local-auth.d.ts +351 -0
  40. package/dist/runtime/server/utils/local-auth.js +43 -0
  41. package/dist/runtime/server/utils/oidc.d.ts +23 -2
  42. package/dist/runtime/server/utils/oidc.js +82 -5
  43. package/dist/runtime/server/utils/session.d.ts +30 -9
  44. package/dist/runtime/server/utils/session.js +189 -61
  45. package/dist/runtime/types.d.ts +12 -0
  46. package/package.json +3 -1
package/README.md CHANGED
@@ -1,84 +1,149 @@
1
- <!--
2
- Get your module up and running quickly.
1
+ # @thecodeorigin/auth
2
+
3
+ Shared Nuxt authentication and authorization for THECODEORIGIN applications.
4
+ ID verifies the account through OIDC authorization code + PKCE. Each application
5
+ then creates its own Better Auth session in its own KV storage.
6
+
7
+ ## Session behavior
8
+
9
+ - Signed, host-only, application-specific opaque cookies identify KV sessions.
10
+ Better Auth adds `__Secure-` to the configured cookie name on HTTPS; consumers
11
+ should use the session API rather than reading the configured base name.
12
+ - Identity, organization access, abilities and entitlement are captured at login.
13
+ Organization switching selects the saved organization abilities locally.
14
+ - Sessions have an absolute expiry (seven days by default). Reading a session,
15
+ using an application, or renewing an ID session never extends that deadline.
16
+ - Signing out of ID or choosing a different ID account does not affect an
17
+ application's existing session. Signing out of an application ends only that
18
+ application's session.
19
+ - Permission changes in ID take effect when the application authenticates again.
20
+ Set a shorter `sessionMaxAge` if the application needs a shorter snapshot lifetime.
21
+ - Directory lookups and support impersonation are explicit online ID operations.
22
+ An expired ID access credential can prevent those operations; it does not
23
+ invalidate local application authentication or ordinary authorization.
24
+ - Account replacement and impersonation rotate the application session. The
25
+ administrator's restoration handle retains the original session expiry.
26
+
27
+ Better Auth owns session creation, signed cookies, KV reads and revocation. Its
28
+ internal establishment endpoint is never exposed as an HTTP route. The module
29
+ preserves the registered `/auth/callback` URI and validates ID-token signature,
30
+ issuer, audience, nonce and subject in addition to PKCE and state.
31
+
32
+ ## Configure a Nuxt application
33
+
34
+ ```ts
35
+ export default defineNuxtConfig({
36
+ modules: ['@thecodeorigin/auth', '@nuxthub/core'],
37
+ hub: { kv: true },
38
+ auth: {
39
+ domain: 'id.thecodeorigin.com',
40
+ sessionStorageBase: 'kv',
41
+ sessionCookieName: 'tco_my_application_auth',
42
+ sessionMaxAge: 60 * 60 * 24 * 7,
43
+ routes: {
44
+ login: '/auth/login',
45
+ signIn: '/auth/sign-in',
46
+ callback: '/auth/callback',
47
+ home: '/dashboard',
48
+ error: '/auth/login',
49
+ },
50
+ },
51
+ })
52
+ ```
53
+
54
+ Configure `NUXT_PUBLIC_AUTH_CLIENT_ID` and `NUXT_AUTH_CLIENT_SECRET` with the
55
+ application's confidential ID client. `NUXT_AUTH_SESSION_SECRET` may provide a
56
+ separate signing secret of at least 32 characters. If omitted, the module derives
57
+ an application-specific signing key from the client secret using HKDF. Rotating
58
+ that source secret invalidates existing application cookies. No new database,
59
+ ID session table or infrastructure seed is required.
60
+
61
+ Use a unique cookie name per application, including applications on different
62
+ localhost ports. Cookies do not isolate ports. Storage keys also include the
63
+ client ID and cookie namespace. Legacy unsigned module cookies are not accepted;
64
+ the first visit after migration requires authentication.
65
+
66
+ Cloudflare KV is eventually consistent. Server expiry is authoritative even if
67
+ KV retains a record. Revocation and concurrent mutations are not globally atomic;
68
+ the module rechecks expected session and login intent before publishing changes.
69
+
70
+ ## Shared application APIs
71
+
72
+ ```ts
73
+ const auth = useAuth()
74
+ await auth.signIn('/projects')
75
+ await auth.switchAccount('/projects')
76
+ await auth.logout() // revoke locally, clear cached data, navigate to the signed-out screen
77
+ await auth.switchOrganization(organizationId)
78
+
79
+ // Login cards retain application presentation; the module owns flow and errors.
80
+ const { error, loggedOut, loading, handleSignIn, handleUseAnotherAccount } = useAuthLogin()
81
+ ```
3
82
 
4
- Find and replace all on all files (CMD+SHIFT+F):
5
- - Name: My Module
6
- - Package name: my-module
7
- - Description: My new Nuxt module
8
- -->
83
+ `signOut()` revokes the local session without navigation. It throws on failure;
84
+ callers must not display successful logout after a failed revocation.
85
+ `logout()` additionally performs a full navigation. Account and impersonation UI
86
+ should also use full navigation after successful identity changes to reset stores.
87
+ The module checks other tabs on focus and session-change notifications.
9
88
 
10
- # My Module
89
+ `recoverSession()` is for same-origin business API 401 handling. It returns false
90
+ only after confirming that the local session is absent. Network/service errors
91
+ preserve existing state. Capture `useAuth()` in a valid Nuxt context before the
92
+ request, and never call `signOut()` merely because an unrelated API returned 401.
11
93
 
12
- [![npm version][npm-version-src]][npm-version-href]
13
- [![npm downloads][npm-downloads-src]][npm-downloads-href]
14
- [![License][license-src]][license-href]
15
- [![Nuxt][nuxt-src]][nuxt-href]
94
+ Server helpers remain available through auto-imports and `@thecodeorigin/auth/server`:
95
+ `getServerAuthSession`, `defineAuthenticatedHandler`, `defineAuthorizedHandler`,
96
+ `defineSensitiveAuthorizedHandler`, `defineAdminHandler`, and CASL helpers.
97
+ `getFreshServerAuthSession` remains a compatibility alias for a current local KV
98
+ read; `force` does not contact ID. All authorization checks use the same snapshot.
16
99
 
17
- My new Nuxt module for doing amazing things.
100
+ Local session mutations require same-origin POST requests. The legacy GET logout
101
+ route directs users to the login surface without mutating authentication.
18
102
 
19
- - [✨ &nbsp;Release Notes](/CHANGELOG.md)
20
- <!-- - [🏀 Online playground](https://stackblitz.com/github/your-org/my-module?file=playground%2Fapp.vue) -->
21
- <!-- - [📖 &nbsp;Documentation](https://example.com) -->
103
+ ## Work on the module across the ecosystem locally
104
+
105
+ From `packages/auth`, install dependencies and build:
106
+
107
+ ```sh
108
+ pnpm install
109
+ pnpm prepack
110
+ ```
22
111
 
23
- ## Features
112
+ From the ecosystem superproject:
24
113
 
25
- <!-- Highlight some of the features your module provide here -->
26
- - ⛰ &nbsp;Foo
27
- - 🚠 &nbsp;Bar
28
- - 🌲 &nbsp;Baz
114
+ ```sh
115
+ node .ecosystem/scripts/auth-local.mjs link
116
+ node .ecosystem/scripts/auth-local.mjs status
117
+ export PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN=false
118
+ ```
119
+
120
+ The helper points all seven consumer `node_modules/@thecodeorigin/auth` links at
121
+ the built local package and records their previous targets under ignored local
122
+ state. It changes no dependency manifests, lockfiles, deployments or gitlinks.
123
+ The environment setting prevents pnpm's automatic dependency verification during
124
+ local Nuxt hooks from replacing the links with published packages. Rebuild the
125
+ module after edits and restart local consumer dev servers. An explicit install
126
+ may replace links; run `link` again afterward.
127
+
128
+ Restore installed versions with:
129
+
130
+ ```sh
131
+ node .ecosystem/scripts/auth-local.mjs unlink
132
+ ```
29
133
 
30
- ## Quick Setup
134
+ Publishing the new module and updating each application's version pin is a
135
+ separate release step. The local link is for this ecosystem checkout, not a
136
+ replacement for a published dependency in independently cloned applications.
137
+ Vault's existing integration-key contracts are included in the source module.
31
138
 
32
- Install the module to your Nuxt application with one command:
139
+ ## Verify
33
140
 
34
- ```bash
35
- npx nuxt module add my-module
141
+ ```sh
142
+ pnpm test
143
+ pnpm lint
144
+ pnpm test:types
36
145
  ```
37
146
 
38
- That's it! You can now use My Module in your Nuxt app ✨
39
-
40
-
41
- ## Contribution
42
-
43
- <details>
44
- <summary>Local development</summary>
45
-
46
- ```bash
47
- # Install dependencies
48
- npm install
49
-
50
- # Generate type stubs
51
- npm run dev:prepare
52
-
53
- # Develop with the playground
54
- npm run dev
55
-
56
- # Build the playground
57
- npm run dev:build
58
-
59
- # Run ESLint
60
- npm run lint
61
-
62
- # Run Vitest
63
- npm run test
64
- npm run test:watch
65
-
66
- # Release new version
67
- npm run release
68
- ```
69
-
70
- </details>
71
-
72
-
73
- <!-- Badges -->
74
- [npm-version-src]: https://img.shields.io/npm/v/my-module/latest.svg?style=flat&colorA=020420&colorB=00DC82
75
- [npm-version-href]: https://npmjs.com/package/my-module
76
-
77
- [npm-downloads-src]: https://img.shields.io/npm/dm/my-module.svg?style=flat&colorA=020420&colorB=00DC82
78
- [npm-downloads-href]: https://npm.chart.dev/my-module
79
-
80
- [license-src]: https://img.shields.io/npm/l/my-module.svg?style=flat&colorA=020420&colorB=00DC82
81
- [license-href]: https://npmjs.com/package/my-module
82
-
83
- [nuxt-src]: https://img.shields.io/badge/Nuxt-020420?logo=nuxt
84
- [nuxt-href]: https://nuxt.com
147
+ Focused tests exercise real Better Auth sessions across separate instances with
148
+ shared KV, complete callback validation, fixed expiry, local organization access,
149
+ account rotation, independent logout, support restoration and client recovery.
@@ -38,6 +38,11 @@ declare const UserinfoClaimsSchema: z.ZodObject<{
38
38
  subject: z.ZodString;
39
39
  conditions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
40
40
  }, z.core.$strip>>;
41
+ organizationAuthorizations: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
42
+ action: z.ZodString;
43
+ subject: z.ZodString;
44
+ conditions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
45
+ }, z.core.$strip>>>>;
41
46
  role: z.ZodNullable<z.ZodString>;
42
47
  }, z.core.$strip>;
43
48
  type UserinfoClaims = z.infer<typeof UserinfoClaimsSchema>;
@@ -102,7 +107,8 @@ declare const OrganizationContextSchema: z.ZodObject<{
102
107
  }, z.core.$strip>>;
103
108
  }, z.core.$strip>;
104
109
  type OrganizationContext = z.infer<typeof OrganizationContextSchema>;
105
- declare const IntegrationKeyContextSchema: z.ZodObject<{
110
+ declare const EnvironmentIntegrationKeyContextSchema: z.ZodObject<{
111
+ createdBy: z.ZodOptional<z.ZodString>;
106
112
  keyId: z.ZodString;
107
113
  organizationId: z.ZodString;
108
114
  clientId: z.ZodString;
@@ -113,10 +119,80 @@ declare const IntegrationKeyContextSchema: z.ZodObject<{
113
119
  name: z.ZodString;
114
120
  prefix: z.ZodString;
115
121
  expiresAt: z.ZodNullable<z.ZodNumber>;
116
- }, z.core.$strip>;
122
+ }, z.core.$strict>;
123
+ declare const EnvironmentCapabilitySchema: z.ZodObject<{
124
+ resourceType: z.ZodLiteral<"environment">;
125
+ resourceId: z.ZodUUID;
126
+ actions: z.ZodArray<z.ZodEnum<{
127
+ "secrets:read": "secrets:read";
128
+ "secrets:create": "secrets:create";
129
+ }>>;
130
+ keyPrefixes: z.ZodOptional<z.ZodArray<z.ZodString>>;
131
+ }, z.core.$strict>;
132
+ declare const DeploymentIntegrationKeyContextSchema: z.ZodObject<{
133
+ keyId: z.ZodString;
134
+ organizationId: z.ZodString;
135
+ version: z.ZodLiteral<2>;
136
+ clientId: z.ZodString;
137
+ audience: z.ZodLiteral<"vault">;
138
+ createdBy: z.ZodString;
139
+ capabilities: z.ZodArray<z.ZodObject<{
140
+ resourceType: z.ZodLiteral<"environment">;
141
+ resourceId: z.ZodUUID;
142
+ actions: z.ZodArray<z.ZodEnum<{
143
+ "secrets:read": "secrets:read";
144
+ "secrets:create": "secrets:create";
145
+ }>>;
146
+ keyPrefixes: z.ZodOptional<z.ZodArray<z.ZodString>>;
147
+ }, z.core.$strict>>;
148
+ permissions: z.ZodArray<z.ZodEnum<{
149
+ "secrets:read": "secrets:read";
150
+ "secrets:create": "secrets:create";
151
+ }>>;
152
+ name: z.ZodString;
153
+ prefix: z.ZodString;
154
+ expiresAt: z.ZodNumber;
155
+ }, z.core.$strict>;
156
+ declare const IntegrationKeyContextSchema: z.ZodUnion<readonly [z.ZodObject<{
157
+ createdBy: z.ZodOptional<z.ZodString>;
158
+ keyId: z.ZodString;
159
+ organizationId: z.ZodString;
160
+ clientId: z.ZodString;
161
+ audience: z.ZodLiteral<"vault">;
162
+ resourceType: z.ZodLiteral<"environment">;
163
+ resourceId: z.ZodString;
164
+ permissions: z.ZodArray<z.ZodLiteral<"secrets:read">>;
165
+ name: z.ZodString;
166
+ prefix: z.ZodString;
167
+ expiresAt: z.ZodNullable<z.ZodNumber>;
168
+ }, z.core.$strict>, z.ZodObject<{
169
+ keyId: z.ZodString;
170
+ organizationId: z.ZodString;
171
+ version: z.ZodLiteral<2>;
172
+ clientId: z.ZodString;
173
+ audience: z.ZodLiteral<"vault">;
174
+ createdBy: z.ZodString;
175
+ capabilities: z.ZodArray<z.ZodObject<{
176
+ resourceType: z.ZodLiteral<"environment">;
177
+ resourceId: z.ZodUUID;
178
+ actions: z.ZodArray<z.ZodEnum<{
179
+ "secrets:read": "secrets:read";
180
+ "secrets:create": "secrets:create";
181
+ }>>;
182
+ keyPrefixes: z.ZodOptional<z.ZodArray<z.ZodString>>;
183
+ }, z.core.$strict>>;
184
+ permissions: z.ZodArray<z.ZodEnum<{
185
+ "secrets:read": "secrets:read";
186
+ "secrets:create": "secrets:create";
187
+ }>>;
188
+ name: z.ZodString;
189
+ prefix: z.ZodString;
190
+ expiresAt: z.ZodNumber;
191
+ }, z.core.$strict>]>;
117
192
  type IntegrationKeyContext = z.infer<typeof IntegrationKeyContextSchema>;
118
193
  /** Server-safe session projection — no tokens, no KV internals. */
119
194
  interface ServerAuthSession {
195
+ expiresAt?: number;
120
196
  sub: string;
121
197
  email: string;
122
198
  name: string | null;
@@ -137,6 +213,7 @@ interface ServerAuthSession {
137
213
  }
138
214
  /** Browser-visible session — NO tokens. */
139
215
  declare const PublicSessionSchema: z.ZodObject<{
216
+ expiresAt: z.ZodOptional<z.ZodNumber>;
140
217
  user: z.ZodObject<{
141
218
  sub: z.ZodString;
142
219
  email: z.ZodString;
@@ -173,5 +250,5 @@ declare const PublicSessionSchema: z.ZodObject<{
173
250
  }, z.core.$strip>;
174
251
  type PublicSession = z.infer<typeof PublicSessionSchema>;
175
252
 
176
- export { AbilityRuleSchema, DirectoryUserPageSchema, DirectoryUserSchema, ImpersonationCandidateSchema, IntegrationKeyContextSchema, OrganizationContextSchema, PublicSessionSchema, PublicUserSchema, RpOrganizationSchema, UserinfoClaimsSchema };
253
+ export { AbilityRuleSchema, DeploymentIntegrationKeyContextSchema, DirectoryUserPageSchema, DirectoryUserSchema, EnvironmentCapabilitySchema, EnvironmentIntegrationKeyContextSchema, ImpersonationCandidateSchema, IntegrationKeyContextSchema, OrganizationContextSchema, PublicSessionSchema, PublicUserSchema, RpOrganizationSchema, UserinfoClaimsSchema };
177
254
  export type { AbilityRule, DirectoryUser, DirectoryUserPage, ImpersonationCandidate, IntegrationKeyContext, OrganizationContext, PublicSession, PublicUser, RpOrganization, ServerAuthSession, UserinfoClaims };
@@ -38,6 +38,11 @@ declare const UserinfoClaimsSchema: z.ZodObject<{
38
38
  subject: z.ZodString;
39
39
  conditions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
40
40
  }, z.core.$strip>>;
41
+ organizationAuthorizations: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
42
+ action: z.ZodString;
43
+ subject: z.ZodString;
44
+ conditions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
45
+ }, z.core.$strip>>>>;
41
46
  role: z.ZodNullable<z.ZodString>;
42
47
  }, z.core.$strip>;
43
48
  type UserinfoClaims = z.infer<typeof UserinfoClaimsSchema>;
@@ -102,7 +107,8 @@ declare const OrganizationContextSchema: z.ZodObject<{
102
107
  }, z.core.$strip>>;
103
108
  }, z.core.$strip>;
104
109
  type OrganizationContext = z.infer<typeof OrganizationContextSchema>;
105
- declare const IntegrationKeyContextSchema: z.ZodObject<{
110
+ declare const EnvironmentIntegrationKeyContextSchema: z.ZodObject<{
111
+ createdBy: z.ZodOptional<z.ZodString>;
106
112
  keyId: z.ZodString;
107
113
  organizationId: z.ZodString;
108
114
  clientId: z.ZodString;
@@ -113,10 +119,80 @@ declare const IntegrationKeyContextSchema: z.ZodObject<{
113
119
  name: z.ZodString;
114
120
  prefix: z.ZodString;
115
121
  expiresAt: z.ZodNullable<z.ZodNumber>;
116
- }, z.core.$strip>;
122
+ }, z.core.$strict>;
123
+ declare const EnvironmentCapabilitySchema: z.ZodObject<{
124
+ resourceType: z.ZodLiteral<"environment">;
125
+ resourceId: z.ZodUUID;
126
+ actions: z.ZodArray<z.ZodEnum<{
127
+ "secrets:read": "secrets:read";
128
+ "secrets:create": "secrets:create";
129
+ }>>;
130
+ keyPrefixes: z.ZodOptional<z.ZodArray<z.ZodString>>;
131
+ }, z.core.$strict>;
132
+ declare const DeploymentIntegrationKeyContextSchema: z.ZodObject<{
133
+ keyId: z.ZodString;
134
+ organizationId: z.ZodString;
135
+ version: z.ZodLiteral<2>;
136
+ clientId: z.ZodString;
137
+ audience: z.ZodLiteral<"vault">;
138
+ createdBy: z.ZodString;
139
+ capabilities: z.ZodArray<z.ZodObject<{
140
+ resourceType: z.ZodLiteral<"environment">;
141
+ resourceId: z.ZodUUID;
142
+ actions: z.ZodArray<z.ZodEnum<{
143
+ "secrets:read": "secrets:read";
144
+ "secrets:create": "secrets:create";
145
+ }>>;
146
+ keyPrefixes: z.ZodOptional<z.ZodArray<z.ZodString>>;
147
+ }, z.core.$strict>>;
148
+ permissions: z.ZodArray<z.ZodEnum<{
149
+ "secrets:read": "secrets:read";
150
+ "secrets:create": "secrets:create";
151
+ }>>;
152
+ name: z.ZodString;
153
+ prefix: z.ZodString;
154
+ expiresAt: z.ZodNumber;
155
+ }, z.core.$strict>;
156
+ declare const IntegrationKeyContextSchema: z.ZodUnion<readonly [z.ZodObject<{
157
+ createdBy: z.ZodOptional<z.ZodString>;
158
+ keyId: z.ZodString;
159
+ organizationId: z.ZodString;
160
+ clientId: z.ZodString;
161
+ audience: z.ZodLiteral<"vault">;
162
+ resourceType: z.ZodLiteral<"environment">;
163
+ resourceId: z.ZodString;
164
+ permissions: z.ZodArray<z.ZodLiteral<"secrets:read">>;
165
+ name: z.ZodString;
166
+ prefix: z.ZodString;
167
+ expiresAt: z.ZodNullable<z.ZodNumber>;
168
+ }, z.core.$strict>, z.ZodObject<{
169
+ keyId: z.ZodString;
170
+ organizationId: z.ZodString;
171
+ version: z.ZodLiteral<2>;
172
+ clientId: z.ZodString;
173
+ audience: z.ZodLiteral<"vault">;
174
+ createdBy: z.ZodString;
175
+ capabilities: z.ZodArray<z.ZodObject<{
176
+ resourceType: z.ZodLiteral<"environment">;
177
+ resourceId: z.ZodUUID;
178
+ actions: z.ZodArray<z.ZodEnum<{
179
+ "secrets:read": "secrets:read";
180
+ "secrets:create": "secrets:create";
181
+ }>>;
182
+ keyPrefixes: z.ZodOptional<z.ZodArray<z.ZodString>>;
183
+ }, z.core.$strict>>;
184
+ permissions: z.ZodArray<z.ZodEnum<{
185
+ "secrets:read": "secrets:read";
186
+ "secrets:create": "secrets:create";
187
+ }>>;
188
+ name: z.ZodString;
189
+ prefix: z.ZodString;
190
+ expiresAt: z.ZodNumber;
191
+ }, z.core.$strict>]>;
117
192
  type IntegrationKeyContext = z.infer<typeof IntegrationKeyContextSchema>;
118
193
  /** Server-safe session projection — no tokens, no KV internals. */
119
194
  interface ServerAuthSession {
195
+ expiresAt?: number;
120
196
  sub: string;
121
197
  email: string;
122
198
  name: string | null;
@@ -137,6 +213,7 @@ interface ServerAuthSession {
137
213
  }
138
214
  /** Browser-visible session — NO tokens. */
139
215
  declare const PublicSessionSchema: z.ZodObject<{
216
+ expiresAt: z.ZodOptional<z.ZodNumber>;
140
217
  user: z.ZodObject<{
141
218
  sub: z.ZodString;
142
219
  email: z.ZodString;
@@ -173,5 +250,5 @@ declare const PublicSessionSchema: z.ZodObject<{
173
250
  }, z.core.$strip>;
174
251
  type PublicSession = z.infer<typeof PublicSessionSchema>;
175
252
 
176
- export { AbilityRuleSchema, DirectoryUserPageSchema, DirectoryUserSchema, ImpersonationCandidateSchema, IntegrationKeyContextSchema, OrganizationContextSchema, PublicSessionSchema, PublicUserSchema, RpOrganizationSchema, UserinfoClaimsSchema };
253
+ export { AbilityRuleSchema, DeploymentIntegrationKeyContextSchema, DirectoryUserPageSchema, DirectoryUserSchema, EnvironmentCapabilitySchema, EnvironmentIntegrationKeyContextSchema, ImpersonationCandidateSchema, IntegrationKeyContextSchema, OrganizationContextSchema, PublicSessionSchema, PublicUserSchema, RpOrganizationSchema, UserinfoClaimsSchema };
177
254
  export type { AbilityRule, DirectoryUser, DirectoryUserPage, ImpersonationCandidate, IntegrationKeyContext, OrganizationContext, PublicSession, PublicUser, RpOrganization, ServerAuthSession, UserinfoClaims };
@@ -25,6 +25,8 @@ const UserinfoClaimsSchema = z.object({
25
25
  }).nullable(),
26
26
  organizations: z.array(RpOrganizationSchema),
27
27
  abilities: z.array(AbilityRuleSchema),
28
+ /** Per-organization snapshot captured during authentication, never inferred from role names. */
29
+ organizationAuthorizations: z.record(z.string(), z.array(AbilityRuleSchema)).default({}),
28
30
  role: z.string().nullable()
29
31
  });
30
32
  const PublicUserSchema = z.object({
@@ -56,7 +58,8 @@ const OrganizationContextSchema = z.object({
56
58
  systemRole: z.string().nullable(),
57
59
  entitlement: UserinfoClaimsSchema.shape.entitlement
58
60
  });
59
- const IntegrationKeyContextSchema = z.object({
61
+ const EnvironmentIntegrationKeyContextSchema = z.object({
62
+ createdBy: z.string().min(1).optional(),
60
63
  keyId: z.string().min(1),
61
64
  organizationId: z.string().min(1),
62
65
  clientId: z.string().min(1),
@@ -67,8 +70,37 @@ const IntegrationKeyContextSchema = z.object({
67
70
  name: z.string().min(1).max(100),
68
71
  prefix: z.string().min(1),
69
72
  expiresAt: z.number().int().positive().nullable()
73
+ }).strict();
74
+ const EnvironmentCapabilitySchema = z.object({
75
+ resourceType: z.literal("environment"),
76
+ resourceId: z.uuid(),
77
+ actions: z.array(z.enum(["secrets:read", "secrets:create"])).min(1).max(2),
78
+ keyPrefixes: z.array(z.string().min(1).max(128).regex(/^[A-Z_][A-Z0-9_]*$/)).min(1).max(20).optional()
79
+ }).strict().superRefine((value, context) => {
80
+ if (new Set(value.actions).size !== value.actions.length || value.actions.includes("secrets:create") !== Boolean(value.keyPrefixes?.length))
81
+ context.addIssue({ code: "custom", message: "Invalid environment capability" });
82
+ });
83
+ const DeploymentIntegrationKeyContextSchema = z.object({
84
+ keyId: z.string().min(1),
85
+ organizationId: z.string().min(1),
86
+ version: z.literal(2),
87
+ clientId: z.string().min(1),
88
+ audience: z.literal("vault"),
89
+ createdBy: z.string().min(1),
90
+ capabilities: z.array(EnvironmentCapabilitySchema).min(1).max(100),
91
+ permissions: z.array(z.enum(["secrets:read", "secrets:create"])).min(1).max(2),
92
+ name: z.string().min(1).max(100),
93
+ prefix: z.string().min(1),
94
+ expiresAt: z.number().int().positive()
95
+ }).strict().superRefine((value, context) => {
96
+ const actions = new Set(value.capabilities.flatMap((capability) => capability.actions));
97
+ if (new Set(value.capabilities.map((capability) => capability.resourceId)).size !== value.capabilities.length || new Set(value.permissions).size !== value.permissions.length || value.permissions.length !== actions.size || value.permissions.some((permission) => !actions.has(permission))) {
98
+ context.addIssue({ code: "custom", message: "Invalid environment capabilities" });
99
+ }
70
100
  });
101
+ const IntegrationKeyContextSchema = z.union([EnvironmentIntegrationKeyContextSchema, DeploymentIntegrationKeyContextSchema]);
71
102
  const PublicSessionSchema = z.object({
103
+ expiresAt: z.number().optional(),
72
104
  user: PublicUserSchema,
73
105
  abilities: z.array(AbilityRuleSchema),
74
106
  systemRole: z.string().nullable(),
@@ -78,4 +110,4 @@ const PublicSessionSchema = z.object({
78
110
  impersonator: PublicUserSchema.nullable()
79
111
  });
80
112
 
81
- export { AbilityRuleSchema, DirectoryUserPageSchema, DirectoryUserSchema, ImpersonationCandidateSchema, IntegrationKeyContextSchema, OrganizationContextSchema, PublicSessionSchema, PublicUserSchema, RpOrganizationSchema, UserinfoClaimsSchema };
113
+ export { AbilityRuleSchema, DeploymentIntegrationKeyContextSchema, DirectoryUserPageSchema, DirectoryUserSchema, EnvironmentCapabilitySchema, EnvironmentIntegrationKeyContextSchema, ImpersonationCandidateSchema, IntegrationKeyContextSchema, OrganizationContextSchema, PublicSessionSchema, PublicUserSchema, RpOrganizationSchema, UserinfoClaimsSchema };
package/dist/module.d.mts CHANGED
@@ -13,6 +13,8 @@ interface ModuleOptions {
13
13
  scopes?: string[];
14
14
  sessionStorageBase?: string;
15
15
  sessionCookieName?: string;
16
+ /** Absolute session lifetime in seconds; activity never extends it. */
17
+ sessionMaxAge?: number;
16
18
  logoutIntentCookieName?: string;
17
19
  routes?: Partial<AuthRoutes>;
18
20
  }
@@ -20,6 +22,8 @@ declare module '@nuxt/schema' {
20
22
  interface RuntimeConfig {
21
23
  auth: {
22
24
  clientSecret: string;
25
+ sessionSecret: string;
26
+ sessionMaxAge: number;
23
27
  sessionStorageBase: string;
24
28
  sessionCookieName: string;
25
29
  logoutIntentCookieName: string;
package/dist/module.d.ts CHANGED
@@ -13,6 +13,8 @@ interface ModuleOptions {
13
13
  scopes?: string[];
14
14
  sessionStorageBase?: string;
15
15
  sessionCookieName?: string;
16
+ /** Absolute session lifetime in seconds; activity never extends it. */
17
+ sessionMaxAge?: number;
16
18
  logoutIntentCookieName?: string;
17
19
  routes?: Partial<AuthRoutes>;
18
20
  }
@@ -20,6 +22,8 @@ declare module '@nuxt/schema' {
20
22
  interface RuntimeConfig {
21
23
  auth: {
22
24
  clientSecret: string;
25
+ sessionSecret: string;
26
+ sessionMaxAge: number;
23
27
  sessionStorageBase: string;
24
28
  sessionCookieName: string;
25
29
  logoutIntentCookieName: string;
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@thecodeorigin/auth",
3
3
  "configKey": "auth",
4
- "version": "0.0.10",
4
+ "version": "0.0.11",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,34 +1,53 @@
1
- import { defineNuxtModule, createResolver, addServerHandler, addServerImportsDir, addImportsDir, addRouteMiddleware, addPlugin } from '@nuxt/kit';
1
+ import { defineNuxtModule, createResolver, directoryToURL, tryResolveModule, resolveModule, addServerHandler, addServerImportsDir, addImportsDir, addRouteMiddleware, addPlugin } from '@nuxt/kit';
2
2
  import { defu } from 'defu';
3
3
 
4
4
  const module$1 = defineNuxtModule({
5
5
  meta: { name: "@thecodeorigin/auth", configKey: "auth" },
6
6
  defaults: {
7
- domain: "",
7
+ domain: "id.thecodeorigin.com",
8
8
  scopes: ["openid", "profile", "email"],
9
- sessionStorageBase: "auth",
10
- sessionCookieName: "tco_auth",
11
- routes: { login: "/auth/login", signIn: "/auth/sign-in", callback: "/auth/callback", signOut: "/auth/sign-out", home: "/", error: "/auth/sign-in" }
9
+ sessionStorageBase: "kv",
10
+ sessionMaxAge: 60 * 60 * 24 * 7,
11
+ routes: { login: "/auth/login", signIn: "/auth/sign-in", callback: "/auth/callback", signOut: "/auth/sign-out", home: "/", error: "/auth/login" }
12
12
  },
13
- setup(options, nuxt) {
13
+ async setup(options, nuxt) {
14
14
  const resolver = createResolver(import.meta.url);
15
+ nuxt.options.alias["@thecodeorigin/auth/server"] = resolver.resolve("./runtime/server/index");
16
+ nuxt.options.alias["@thecodeorigin/auth/contract"] = resolver.resolve("./contract/index");
17
+ nuxt.options.vite.resolve ??= {};
18
+ nuxt.options.vite.resolve.dedupe = [.../* @__PURE__ */ new Set([
19
+ ...nuxt.options.vite.resolve.dedupe ?? [],
20
+ "@casl/vue",
21
+ "@casl/ability"
22
+ ])];
23
+ const consumerUrl = new URL("package.json", directoryToURL(nuxt.options.rootDir));
24
+ for (const dependency of ["@casl/vue", "@casl/ability"]) {
25
+ if (!nuxt.options.alias[dependency] && !await tryResolveModule(dependency, consumerUrl))
26
+ nuxt.options.alias[dependency] = resolveModule(dependency, { url: new URL(import.meta.url) });
27
+ }
15
28
  const routes = {
16
29
  login: "/auth/login",
17
30
  signIn: "/auth/sign-in",
18
31
  callback: "/auth/callback",
19
32
  signOut: "/auth/sign-out",
20
33
  home: "/",
21
- error: "/auth/sign-in",
34
+ error: "/auth/login",
22
35
  ...options.routes
23
36
  };
24
- const logoutIntentCookieName = options.logoutIntentCookieName ?? `${options.sessionCookieName}_logout`;
37
+ const appName = nuxt.options.appConfig?.name ?? nuxt.options.rootDir.split("/").filter(Boolean).at(-1) ?? "app";
38
+ const sessionCookieName = options.sessionCookieName ?? `tco_${appName.replace(/\W/g, "_")}_auth`;
39
+ const logoutIntentCookieName = options.logoutIntentCookieName ?? `${sessionCookieName}_logout`;
40
+ if (!Number.isSafeInteger(options.sessionMaxAge) || options.sessionMaxAge < 60)
41
+ throw new Error("[auth] sessionMaxAge must be an integer of at least 60 seconds");
25
42
  if (routes.login === routes.signIn) {
26
43
  throw new Error("[auth] routes.login and routes.signIn must use different paths");
27
44
  }
28
45
  nuxt.options.runtimeConfig.auth = defu(nuxt.options.runtimeConfig.auth, {
29
46
  clientSecret: "",
47
+ sessionSecret: "",
48
+ sessionMaxAge: options.sessionMaxAge,
30
49
  sessionStorageBase: options.sessionStorageBase,
31
- sessionCookieName: options.sessionCookieName,
50
+ sessionCookieName,
32
51
  logoutIntentCookieName
33
52
  });
34
53
  nuxt.options.runtimeConfig.public.auth = defu(nuxt.options.runtimeConfig.public.auth, {
@@ -38,11 +57,12 @@ const module$1 = defineNuxtModule({
38
57
  logoutIntentCookieName,
39
58
  scopes: options.scopes
40
59
  });
41
- addServerHandler({ route: routes.signIn, handler: resolver.resolve("./runtime/server/routes/sign-in.get") });
42
- addServerHandler({ route: routes.callback, handler: resolver.resolve("./runtime/server/routes/callback.get") });
43
- addServerHandler({ route: routes.signOut, handler: resolver.resolve("./runtime/server/routes/sign-out.get") });
60
+ addServerHandler({ route: routes.signIn, method: "get", handler: resolver.resolve("./runtime/server/routes/sign-in.get") });
61
+ addServerHandler({ route: routes.callback, method: "get", handler: resolver.resolve("./runtime/server/routes/callback.get") });
62
+ addServerHandler({ route: routes.signOut, method: "get", handler: resolver.resolve("./runtime/server/routes/sign-out.get") });
44
63
  for (const [route, file] of [
45
64
  ["/api/_auth/session", "session.get"],
65
+ ["/api/_auth/session-restore", "session-restore.get"],
46
66
  ["/api/_auth/sign-out", "sign-out.post"],
47
67
  ["/api/_auth/organizations", "organizations.get"],
48
68
  ["/api/_auth/organizations/switch", "organizations/switch.post"],
@@ -62,6 +82,7 @@ const module$1 = defineNuxtModule({
62
82
  addRouteMiddleware({ name: "auth", path: resolver.resolve("./runtime/app/middleware/auth.global"), global: true });
63
83
  addRouteMiddleware({ name: "casl", path: resolver.resolve("./runtime/app/middleware/casl.global"), global: true });
64
84
  addPlugin(resolver.resolve("./runtime/app/plugins/0.session"));
85
+ addPlugin(resolver.resolve("./runtime/app/plugins/session-sync.client"));
65
86
  addPlugin(resolver.resolve("./runtime/app/plugins/ability"));
66
87
  nuxt.hook("prepare:types", ({ references }) => {
67
88
  references.push({ path: resolver.resolve("./runtime/types.d.ts") });