@avelonjs/core 0.1.0 → 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/README.md CHANGED
@@ -159,7 +159,7 @@ export const IndexPublishedPost = defineListener<PostPublished, void, 'queued'>(
159
159
 
160
160
  ## Runtime Facades
161
161
 
162
- Wave B implements the frozen contracts as process-local facades. You wire drivers with `defineConfig`, dispatch events through `Events`, authorize with `Gate`, validate with `defineRequest`, inject wards with `injectWard`, and run background work through `Errands`. Named mailers, disks, and connections never fall back to the default when the name is unknown.
162
+ The runtime implements the frozen contracts as process-local facades. You wire drivers with `defineConfig`, dispatch events through `Events`, authorize with `Gate`, validate with `defineRequest`, inject wards with `injectWard`, and run background work through `Errands`. Named mailers, disks, and connections never fall back to the default when the name is unknown.
163
163
 
164
164
  ```ts
165
165
  import {
@@ -301,6 +301,7 @@ The table lists every public export. Capability surface interfaces describe meth
301
301
  | `DriverContract` | `interface DriverContract<TCapabilities, TRaw>` | Shared name, instance, capabilities, and raw client contract. |
302
302
  | `DriverFault` | `class DriverFault` | Normalized unmapped driver error. |
303
303
  | `DriverFaultMetadata` | `interface DriverFaultMetadata` | Driver contract and operation identifiers. |
304
+ | `EmailVerificationIdentitySurface` | `interface EmailVerificationIdentitySurface` | Send-confirmation and token-verify methods. |
304
305
  | `EmbeddingAiSurface` | `interface EmbeddingAiSurface` | Text embedding method. |
305
306
  | `EmbeddingResult` | `interface EmbeddingResult` | Ordered embedding vectors and token count. |
306
307
  | `ErrorCode` | `type ErrorCode` | Fixed machine-readable error code union. |
@@ -320,8 +321,8 @@ The table lists every public export. Capability surface interfaces describe meth
320
321
  | `ForbiddenMetadata` | `interface ForbiddenMetadata` | Denied ability and resource identifiers. |
321
322
  | `HttpMethod` | `type HttpMethod` | Methods accepted by route manifests. |
322
323
  | `HttpRequest` | `interface HttpRequest<TBody>` | Framework-neutral decoded request with access to raw bytes. |
323
- | `Identity` | `type Identity<TDriver>` | Identity facade narrowed to five auth capabilities. |
324
- | `IdentityCapabilities` | `interface IdentityCapabilities<TFactor>` | Password, magic-link, OAuth, organization, and MFA factor declaration. |
324
+ | `Identity` | `type Identity<TDriver>` | Identity facade narrowed to six auth capabilities. |
325
+ | `IdentityCapabilities` | `interface IdentityCapabilities<TFactor>` | Password, magic-link, OAuth, organization, MFA, and email-verification declaration. |
325
326
  | `IdentityDriver` | `interface IdentityDriver<TCapabilities, TRaw, TActor, TSession>` | Current actor, session, and sign-out contract. |
326
327
  | `IdentityOrganization` | `interface IdentityOrganization` | Normalized organization summary. |
327
328
  | `Invalid` | `class Invalid` | Normalized invalid-input error. |
@@ -339,7 +340,7 @@ The table lists every public export. Capability surface interfaces describe meth
339
340
  | `LogLevel` | `type LogLevel` | Portable structured log severity. |
340
341
  | `LogRecord` | `interface LogRecord` | Structured application log event. |
341
342
  | `Logs` | `type Logs<TDriver>` | Logging facade narrowed to tracing support. |
342
- | `MagicLinkIdentitySurface` | `interface MagicLinkIdentitySurface` | Passwordless sign-in link method. |
343
+ | `MagicLinkIdentitySurface` | `interface MagicLinkIdentitySurface<TActor>` | Passwordless sign-in: send a link, then redeem its token. |
343
344
  | `Mail` | `type Mail<TDriver>` | Mail facade narrowed to hosted templates. |
344
345
  | `MailAttachment` | `interface MailAttachment` | Portable attachment filename, bytes, and media type. |
345
346
  | `MailCapabilities` | `interface MailCapabilities` | Literal hosted-template capability declaration. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avelonjs/core",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "Frozen TypeScript contracts, errors, and capability types shared by Avelon packages.",
6
6
  "license": "MIT",
@@ -12,6 +12,8 @@ export interface IdentityCapabilities<TFactor extends string = string> {
12
12
  readonly organizations: boolean
13
13
  /** Supported factors retained as a literal readonly list; empty means no MFA surface. */
14
14
  readonly mfa: readonly TFactor[]
15
+ /** Whether email-address confirmation is available. */
16
+ readonly emailVerification: boolean
15
17
  }
16
18
 
17
19
  /** Authentication capability alias matching the application-facing `Auth` vocabulary. */
@@ -77,9 +79,11 @@ export interface PasswordIdentitySurface<TActor = unknown> {
77
79
  }
78
80
 
79
81
  /** Passwordless operations exposed only by magic-link-capable identity drivers. */
80
- export interface MagicLinkIdentitySurface {
82
+ export interface MagicLinkIdentitySurface<TActor = unknown> {
81
83
  /** Sends a sign-in link to an email address. */
82
84
  sendMagicLink(email: string, redirectTo?: string): Promise<void>
85
+ /** Authenticates an actor with the token a magic link carried. */
86
+ signInWithMagicLink(token: string): Promise<TActor>
83
87
  }
84
88
 
85
89
  /** OAuth linking exposed only when the identity driver supports linked identities. */
@@ -104,6 +108,14 @@ export interface MfaIdentitySurface<TFactor extends string = string> {
104
108
  verifyMfa(challengeId: string, code: string): Promise<void>
105
109
  }
106
110
 
111
+ /** Email confirmation exposed only by drivers that verify addresses. */
112
+ export interface EmailVerificationIdentitySurface {
113
+ /** Sends a confirmation message without revealing account existence. */
114
+ sendEmailVerification(email?: string): Promise<void>
115
+ /** Confirms an email address after validating a vendor token. */
116
+ verifyEmail(token: string): Promise<void>
117
+ }
118
+
107
119
  type ActorOf<TDriver extends IdentityDriver> =
108
120
  TDriver extends IdentityDriver<IdentityCapabilities, unknown, infer TActor, unknown>
109
121
  ? TActor
@@ -124,10 +136,14 @@ export type Identity<TDriver extends IdentityDriver> = Pick<
124
136
  TDriver['capabilities']['passwords'],
125
137
  PasswordIdentitySurface<ActorOf<TDriver>>
126
138
  > &
127
- CapabilitySurface<TDriver['capabilities']['magicLinks'], MagicLinkIdentitySurface> &
139
+ CapabilitySurface<
140
+ TDriver['capabilities']['magicLinks'],
141
+ MagicLinkIdentitySurface<ActorOf<TDriver>>
142
+ > &
128
143
  CapabilitySurface<TDriver['capabilities']['oauth'], OAuthIdentitySurface> &
129
144
  CapabilitySurface<TDriver['capabilities']['organizations'], OrganizationIdentitySurface> &
130
- MfaIdentityFor<TDriver['capabilities']>
145
+ MfaIdentityFor<TDriver['capabilities']> &
146
+ CapabilitySurface<TDriver['capabilities']['emailVerification'], EmailVerificationIdentitySurface>
131
147
 
132
148
  /** Authentication facade alias matching the application-facing `Auth` vocabulary. */
133
149
  export type Auth<TDriver extends IdentityDriver> = Identity<TDriver>
@@ -36,7 +36,7 @@ export interface AvelonDrivers {
36
36
  export interface AvelonConfig {
37
37
  /** Application name used in diagnostics. */
38
38
  name?: string
39
- /** Optional adapter. Wave C mounts routes; Wave B only stores the reference. */
39
+ /** Optional adapter. The Next adapter mounts routes. Core only stores the reference. */
40
40
  adapter?: Adapter
41
41
  /** Driver wiring for the current environment. */
42
42
  drivers: AvelonDrivers
@@ -1,3 +1,4 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
1
2
  import { Forbidden, Unauthenticated } from '../errors'
2
3
 
3
4
  /** Actor shape accepted by the gate. `null` is an anonymous caller. */
@@ -12,7 +13,18 @@ export type PolicyAbilities = Readonly<Record<string, PolicyHandler>>
12
13
  type ResourceKey = string | { readonly name: string }
13
14
 
14
15
  const policies = new Map<string, PolicyAbilities>()
15
- let currentActor: GateActor = null
16
+ const storage = new AsyncLocalStorage<{ actor: GateActor }>()
17
+ let fallbackActor: GateActor = null
18
+
19
+ function activeActor(): GateActor {
20
+ const store = storage.getStore()
21
+ return store === undefined ? fallbackActor : store.actor
22
+ }
23
+
24
+ /** Runs `fn` with `actor` bound for that call and everything it awaits. */
25
+ export async function runWithActor<T>(actor: GateActor, fn: () => Promise<T>): Promise<T> {
26
+ return await storage.run({ actor }, fn)
27
+ }
16
28
 
17
29
  function resourceName(resource: ResourceKey | object): string {
18
30
  if (typeof resource === 'string') return resource
@@ -39,7 +51,7 @@ export function definePolicy(resource: ResourceKey, abilities: PolicyAbilities):
39
51
  /** Clears registered policies and the request actor. Intended for tests. */
40
52
  export function resetPolicies(): void {
41
53
  policies.clear()
42
- currentActor = null
54
+ fallbackActor = null
43
55
  }
44
56
 
45
57
  async function allows(actor: GateActor, ability: string, resource?: unknown): Promise<boolean> {
@@ -56,21 +68,31 @@ async function allows(actor: GateActor, ability: string, resource?: unknown): Pr
56
68
 
57
69
  /** Authorization helpers used by controllers. */
58
70
  export const Gate = {
59
- /** Binds the request actor used when `authorize`/`can` omit one. */
71
+ /**
72
+ * Binds the request actor used when `authorize`/`can` omit one.
73
+ *
74
+ * Inside `runWithActor` the write stays scoped to that call; outside one it writes the
75
+ * process-wide fallback.
76
+ */
60
77
  setActor(actor: GateActor): void {
61
- currentActor = actor
78
+ const store = storage.getStore()
79
+ if (store !== undefined) {
80
+ store.actor = actor
81
+ return
82
+ }
83
+ fallbackActor = actor
62
84
  },
63
85
 
64
86
  /** Returns the actor bound for this request, or `null`. */
65
87
  actor(): GateActor {
66
- return currentActor
88
+ return activeActor()
67
89
  },
68
90
 
69
91
  /** Returns whether the actor may perform the ability. */
70
92
  async can(
71
93
  ability: string,
72
94
  resource?: unknown,
73
- actor: GateActor = currentActor,
95
+ actor: GateActor = activeActor(),
74
96
  ): Promise<boolean> {
75
97
  return allows(actor, ability, resource)
76
98
  },
@@ -79,7 +101,7 @@ export const Gate = {
79
101
  async authorize(
80
102
  ability: string,
81
103
  resource?: unknown,
82
- actor: GateActor = currentActor,
104
+ actor: GateActor = activeActor(),
83
105
  ): Promise<void> {
84
106
  if (actor === null) {
85
107
  const handlerAllowsAnonymous = await allows(null, ability, resource)
@@ -7,8 +7,16 @@ import type {
7
7
  RouteDefinition,
8
8
  ViewResult,
9
9
  } from '../adapter'
10
- import { Forbidden, Invalid, NotFound, Unauthenticated } from '../errors'
11
- import { Gate, type GateActor } from './gate'
10
+ import {
11
+ Conflict,
12
+ Forbidden,
13
+ Invalid,
14
+ NotFound,
15
+ RateLimited,
16
+ Unauthenticated,
17
+ Unavailable,
18
+ } from '../errors'
19
+ import { Gate, runWithActor, type GateActor } from './gate'
12
20
  import { Events } from './dispatcher'
13
21
  import { flushAfterErrands } from './errands'
14
22
 
@@ -46,7 +54,15 @@ function hasHeader(headers: Readonly<Record<string, string>>, name: string): boo
46
54
  )
47
55
  }
48
56
 
49
- /** Maps framework errors onto transport-neutral kernel results. */
57
+ /**
58
+ * Maps framework errors onto transport-neutral kernel results.
59
+ *
60
+ * Every taxonomy member except `DriverFault` has a result here; that one stays unmapped because an
61
+ * unrecognized driver failure is a server fault, not an answer to give the caller. A member left
62
+ * unmapped propagates out of the dispatcher, and the adapter above it turns that into a runtime
63
+ * error whose message a production build hides, which is how a driver reporting "Signups not
64
+ * allowed for this instance" reaches the browser as a blank failure.
65
+ */
50
66
  export function mapKernelException(error: unknown): KernelResult | undefined {
51
67
  if (error instanceof Invalid) {
52
68
  return {
@@ -63,6 +79,30 @@ export function mapKernelException(error: unknown): KernelResult | undefined {
63
79
  status: 302,
64
80
  } satisfies RedirectResult
65
81
  }
82
+ if (error instanceof Conflict) {
83
+ return {
84
+ type: 'action',
85
+ ok: false,
86
+ errors: { _form: [error.message] },
87
+ status: 409,
88
+ } satisfies ActionResult
89
+ }
90
+ if (error instanceof RateLimited) {
91
+ return {
92
+ type: 'action',
93
+ ok: false,
94
+ errors: { _form: [error.message] },
95
+ status: 429,
96
+ } satisfies ActionResult
97
+ }
98
+ if (error instanceof Unavailable) {
99
+ return {
100
+ type: 'action',
101
+ ok: false,
102
+ errors: { _form: [error.message] },
103
+ status: 503,
104
+ } satisfies ActionResult
105
+ }
66
106
  if (error instanceof Forbidden) {
67
107
  return {
68
108
  type: 'action',
@@ -92,20 +132,20 @@ export function createKernel(options: CreateKernelOptions = {}): Kernel<Controll
92
132
  return {
93
133
  async dispatch(route, request) {
94
134
  try {
95
- if (options.resolveActor) {
96
- Gate.setActor(await options.resolveActor(request))
97
- }
98
- await runMiddleware(route, request, options)
99
- const bindings = await resolveBindings(route, request, options.resolveBinding)
100
- const controller = new route.controller()
101
- const action = Reflect.get(controller, route.action)
102
- if (typeof action !== 'function') {
103
- throw new NotFound(`Controller action ${route.action} was not found.`, {
104
- metadata: { resource: 'controller_action', identifier: route.action },
105
- })
106
- }
107
- const result = await (action as ControllerMethod).call(controller, request, ...bindings)
108
- return normalizeResult(result)
135
+ const actor = options.resolveActor ? await options.resolveActor(request) : Gate.actor()
136
+ return await runWithActor(actor, async () => {
137
+ await runMiddleware(route, request, options)
138
+ const bindings = await resolveBindings(route, request, options.resolveBinding)
139
+ const controller = new route.controller()
140
+ const action = Reflect.get(controller, route.action)
141
+ if (typeof action !== 'function') {
142
+ throw new NotFound(`Controller action ${route.action} was not found.`, {
143
+ metadata: { resource: 'controller_action', identifier: route.action },
144
+ })
145
+ }
146
+ const result = await (action as ControllerMethod).call(controller, request, ...bindings)
147
+ return normalizeResult(result)
148
+ })
109
149
  } catch (error) {
110
150
  const mapped = options.mapException?.(error) ?? mapKernelException(error)
111
151
  if (mapped !== undefined) return mapped