wirejs-resources 0.1.191 → 0.1.192

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
@@ -1,3 +1,114 @@
1
- Experimental.
1
+ # wirejs-resources
2
2
 
3
- If this package interests you, contact the author.
3
+ > **Experimental:** WireJS is an experimental hobby project and is not production software. For a professionally supported project in a similar spirit, see [AWS Blocks](https://github.com/aws-devtools-labs/aws-blocks), which benefits from lessons and ideas explored here.
4
+
5
+ Experimental deployment-neutral resources, request context, and service abstractions for WireJS applications.
6
+
7
+ This package can be used by WireJS apps, API modules, SSR/SSG code, deployment adapters, or other Node code that wants cloud/host/provider-agnostic resource abstractions. App code describes what it needs; local and deployment-specific packages decide how those resources are implemented.
8
+
9
+ ## What is in this package?
10
+
11
+ - `Resource` — the base identity/scope model used by all resources.
12
+ - `Context`, `CookieJar`, `SignedCookie` — request/runtime context and cookie helpers.
13
+ - Data/config resources — `Setting`, `KeyValueStore`, `DistributedTable`.
14
+ - Invocation resources — `Endpoint`, `BackgroundJob`, `CronJob`.
15
+ - Runtime metadata — `SystemAttribute`.
16
+ - Services — `FileService`, `AuthenticationService`, OIDC provider presets, `RealtimeService`, `LLM`, `EmailSender`.
17
+ - Override hooks — deployment packages can swap local implementations for cloud/provider implementations.
18
+
19
+ ## Detailed docs
20
+
21
+ - [Core concepts: resources, scopes, overrides, and context](./docs/core.md)
22
+ - [Data/config resources: Setting, KeyValueStore, DistributedTable](./docs/data-resources.md)
23
+ - [Invocation resources: Endpoint, BackgroundJob, CronJob, SystemAttribute](./docs/invocation-resources.md)
24
+ - [Services: FileService, AuthenticationService, RealtimeService, LLM, EmailSender](./docs/services.md)
25
+
26
+ ## Quick example
27
+
28
+ ```ts
29
+ import {
30
+ Context,
31
+ DistributedTable,
32
+ Endpoint,
33
+ PassThruParser,
34
+ Setting,
35
+ } from 'wirejs-resources';
36
+
37
+ const siteTitle = new Setting('site', 'title', {
38
+ private: false,
39
+ description: 'Human-readable site title',
40
+ init: () => 'My WireJS app',
41
+ });
42
+
43
+ type Todo = {
44
+ userId: string;
45
+ id: string;
46
+ status: 'open' | 'done';
47
+ title: string;
48
+ };
49
+
50
+ const todos = new DistributedTable('app', 'todos', {
51
+ parse: PassThruParser<Todo>,
52
+ key: {
53
+ partition: { field: 'userId', type: 'string' },
54
+ sort: { field: 'id', type: 'string' },
55
+ },
56
+ indexes: [
57
+ { partition: { field: 'status', type: 'string' } },
58
+ ],
59
+ });
60
+
61
+ new Endpoint('api', 'open-todos', {
62
+ path: '/open-todos',
63
+ description: 'List open todos for a user.',
64
+ handle: async (context: Context) => {
65
+ const userId = context.location.searchParams.get('userId') ?? 'anonymous';
66
+ const title = await siteTitle.read();
67
+ const items = [];
68
+
69
+ for await (const todo of todos.query({
70
+ by: 'userId-id',
71
+ where: { userId },
72
+ filter: { status: { eq: 'open' } },
73
+ })) {
74
+ items.push(todo);
75
+ }
76
+
77
+ context.responseHeaders['content-type'] = 'application/json; charset=utf-8';
78
+ return JSON.stringify({ title, items });
79
+ },
80
+ });
81
+ ```
82
+
83
+ ## Exports
84
+
85
+ Common exports include:
86
+
87
+ ```ts
88
+ import {
89
+ Context,
90
+ CookieJar,
91
+ SignedCookie,
92
+ Resource,
93
+ Setting,
94
+ DistributedTable,
95
+ PassThruParser,
96
+ KeyValueStore,
97
+ Endpoint,
98
+ BackgroundJob,
99
+ CronJob,
100
+ SystemAttribute,
101
+ FileService,
102
+ AuthenticationService,
103
+ GoogleOIDCProvider,
104
+ GitHubOIDCProvider,
105
+ RealtimeService,
106
+ LLM,
107
+ EmailSender,
108
+ overrides,
109
+ withContext,
110
+ requiresContext,
111
+ } from 'wirejs-resources';
112
+ ```
113
+
114
+ WireJS is experimental. Expect breaking changes and check the README/docs installed with your exact package version after upgrading.
package/docs/core.md ADDED
@@ -0,0 +1,147 @@
1
+ # Core concepts
2
+
3
+ ## Resource identity and scope
4
+
5
+ `Resource` is the base class for resources and services. A resource is identified by a `scope` and an `id`; together they produce an `absoluteId`.
6
+
7
+ ```ts
8
+ import { Resource } from 'wirejs-resources';
9
+
10
+ const app = new Resource('apps', 'store');
11
+ const child = new Resource(app, 'uploads');
12
+
13
+ console.log(app.absoluteId); // apps/store
14
+ console.log(child.absoluteId); // apps/store/uploads
15
+ ```
16
+
17
+ Use stable IDs. Deployment adapters use `absoluteId` values to provision or locate provider resources. Changing IDs in a deployed app can create new infrastructure or orphan old data.
18
+
19
+ ## Local defaults and deployment overrides
20
+
21
+ The default implementations are intentionally local-development friendly. For example, local `FileService` writes under `temp/wirejs-services`, and local `EmailSender` writes messages to `.outbox`.
22
+
23
+ Deployment packages can replace implementations through `overrides`, allowing app code to keep constructing the same resources while storage, auth, email, realtime, etc. move to cloud/provider-backed implementations.
24
+
25
+ ```ts
26
+ import { overrides, FileService } from 'wirejs-resources';
27
+
28
+ // Deployment packages normally do this, not app code.
29
+ overrides.FileService = class ProviderFileService extends FileService {
30
+ // provider-backed implementation
31
+ };
32
+ ```
33
+
34
+ ## Context
35
+
36
+ `Context` represents a request/runtime invocation. It is used by SSR handlers, endpoints, API methods that need request state, and service code that has to inspect or mutate response details.
37
+
38
+ Important properties:
39
+
40
+ - `location: URL` — current request URL. Assigning a new URL/string marks `locationIsDirty`.
41
+ - `locationIsDirty: boolean` — response layers can use this to emit redirects.
42
+ - `cookies: CookieJar` — mutable request/response cookie jar.
43
+ - `httpMethod?: string`
44
+ - `requestHeaders: Record<string, string | string[]>`
45
+ - `requestBody?: string`
46
+ - `responseHeaders: Record<string, string>`
47
+ - `responseCode?: number`
48
+ - `systemInfo` — iterable of static and runtime `SystemAttribute`s.
49
+
50
+ ```ts
51
+ import type { Context } from 'wirejs-resources';
52
+
53
+ export async function redirectToLogin(context: Context) {
54
+ context.location = new URL('/login', context.location.origin);
55
+ context.responseCode = 302;
56
+ context.responseHeaders['cache-control'] = 'no-store';
57
+ return '';
58
+ }
59
+ ```
60
+
61
+ ## Cookies
62
+
63
+ `CookieJar` parses request cookies and tracks response cookie changes. Use it when endpoint/API/SSR code needs request cookies or needs to set values for the response pipeline.
64
+
65
+ ```ts
66
+ const value = context.cookies.get('theme')?.value;
67
+
68
+ context.cookies.set({
69
+ name: 'theme',
70
+ value: 'dark',
71
+ path: '/',
72
+ maxAge: 60 * 60 * 24 * 365,
73
+ secure: true,
74
+ httpOnly: false,
75
+ });
76
+
77
+ context.cookies.delete('old-cookie');
78
+ ```
79
+
80
+ `getAll()` returns plain request/current cookie values. `getSetCookies()` returns only cookies that were changed and need `Set-Cookie` response headers.
81
+
82
+ ## SignedCookie
83
+
84
+ `SignedCookie<T>` is a public low-level helper for tamper-evident cookie state. It stores the value as a signed JWT in a cookie. Consumers can use it directly when they want small authenticated cookie payloads, but it is not encrypted: do not put secrets in the payload unless you are comfortable with clients being able to read them. For auth specifically, prefer `AuthenticationService` unless you are building a custom auth/session layer.
85
+
86
+ ```ts
87
+ import { Setting, SignedCookie } from 'wirejs-resources';
88
+
89
+ type Flash = { message: string; level: 'info' | 'error' };
90
+
91
+ const signingSecret = new Setting('ui', 'flash-cookie-secret', {
92
+ private: true,
93
+ init: 'random',
94
+ description: 'Signs flash message cookies.',
95
+ });
96
+
97
+ const flashCookie = new SignedCookie<Flash>(
98
+ 'ui',
99
+ 'flash',
100
+ signingSecret,
101
+ {
102
+ maxAge: 60,
103
+ httpOnly: true,
104
+ secure: true,
105
+ path: '/',
106
+ }
107
+ );
108
+
109
+ await flashCookie.write(context.cookies, {
110
+ message: 'Saved successfully.',
111
+ level: 'info',
112
+ });
113
+
114
+ const flash = await flashCookie.read(context.cookies);
115
+ await flashCookie.write(context.cookies, null); // clears the cookie
116
+ ```
117
+
118
+ Behavior:
119
+
120
+ - Cookie names are prefixed by scope, e.g. `ui-flash` or `app-auth-identity`.
121
+ - `read()` returns `T | null`; invalid signatures and expired tokens return `null`.
122
+ - `write(cookies, value)` signs and sets the cookie.
123
+ - `write(cookies, null)` or `clear(cookies)` deletes it.
124
+ - Default signed-cookie options are `httpOnly: true`, `secure: true`, and one-hour max age unless overridden.
125
+
126
+ ## Context-wrapped APIs
127
+
128
+ `withContext()` and `requiresContext()` are helpers for APIs where some methods need the active request context but callers should still see a convenient method namespace.
129
+
130
+ ```ts
131
+ import { Context, withContext } from 'wirejs-resources';
132
+
133
+ export const userApi = withContext((context: Context) => ({
134
+ profile: {
135
+ async current() {
136
+ return { path: context.location.pathname };
137
+ },
138
+ },
139
+ }));
140
+
141
+ // Later, with an active context:
142
+ const profile = await userApi.profile.current(context);
143
+ ```
144
+
145
+ ## System information
146
+
147
+ `SystemAttribute` values expose deployment/runtime facts such as deployment type, local/public origins, or provider capabilities. Static attributes are registered by code; runtime attributes are supplied by hosting/deployment layers and exposed through `context.systemInfo`.
@@ -0,0 +1,182 @@
1
+ # Data and configuration resources
2
+
3
+ ## Setting
4
+
5
+ `Setting` represents a typed configuration value. Settings are stored in an internal `KeyValueStore` and are registered at construction time so admin/config tools can discover them.
6
+
7
+ ```ts
8
+ import { Setting } from 'wirejs-resources';
9
+
10
+ export const theme = new Setting('site', 'theme', {
11
+ private: false,
12
+ description: 'Public site theme',
13
+ options: ['light', 'dark'] as const,
14
+ init: () => 'light',
15
+ });
16
+
17
+ const current = await theme.read(); // 'light' | 'dark' | undefined
18
+ await theme.write('dark');
19
+ ```
20
+
21
+ Options:
22
+
23
+ - `private` — marks secrets or internal settings. UIs should avoid displaying values for private settings.
24
+ - `description` — human-readable explanation.
25
+ - `options` — valid string options; also improves TypeScript inference.
26
+ - `init` — optional lazy initializer. Use a function for defaults or `'random'` for generated secret material.
27
+
28
+ ```ts
29
+ const jwtSecret = new Setting('auth', 'jwt-secret', {
30
+ private: true,
31
+ description: 'Signs auth/session tokens.',
32
+ init: 'random',
33
+ });
34
+ ```
35
+
36
+ `read()` initializes the setting only when no value exists. `write(value, { onlyIfNotExists: true })` is useful when multiple processes may race to initialize the same value.
37
+
38
+ Discovery:
39
+
40
+ ```ts
41
+ for (const setting of Setting.list()) {
42
+ console.log(setting.absoluteId, setting.description, setting.isPrivate);
43
+ }
44
+ ```
45
+
46
+ ## KeyValueStore
47
+
48
+ `KeyValueStore<T>` is a simple string-keyed data store.
49
+
50
+ ```ts
51
+ import { KeyValueStore } from 'wirejs-resources';
52
+
53
+ type Session = { userId: string; expiresAt: string };
54
+ const sessions = new KeyValueStore<Session>('auth', 'sessions');
55
+
56
+ await sessions.set('session-id', {
57
+ userId: 'user-1',
58
+ expiresAt: new Date(Date.now() + 3600_000).toISOString(),
59
+ });
60
+
61
+ const session = await sessions.get('session-id');
62
+ await sessions.delete('session-id');
63
+ ```
64
+
65
+ Scanning:
66
+
67
+ ```ts
68
+ for await (const { key, value } of sessions.scan()) {
69
+ console.log(key, value.userId);
70
+ }
71
+ ```
72
+
73
+ Use `KeyValueStore` for simple lookup data. Use `DistributedTable` when records need typed keys, range/query patterns, indexes, or filtering.
74
+
75
+ ## DistributedTable
76
+
77
+ `DistributedTable` is a deployment-neutral table abstraction for structured records. It favors provider mappings that scale across partitions, similar to DynamoDB-style table design.
78
+
79
+ ```ts
80
+ import { DistributedTable, PassThruParser } from 'wirejs-resources';
81
+
82
+ type Todo = {
83
+ userId: string;
84
+ id: string;
85
+ status: 'open' | 'done';
86
+ createdAt: string;
87
+ title: string;
88
+ };
89
+
90
+ export const todos = new DistributedTable('app', 'todos', {
91
+ parse: PassThruParser<Todo>,
92
+ key: {
93
+ partition: { field: 'userId', type: 'string' },
94
+ sort: { field: 'id', type: 'string' },
95
+ },
96
+ indexes: [
97
+ {
98
+ partition: { field: 'status', type: 'string' },
99
+ sort: { field: 'createdAt', type: 'string' },
100
+ },
101
+ ],
102
+ });
103
+ ```
104
+
105
+ ### Parser
106
+
107
+ `parse` runs when records are read. Use `PassThruParser<T>` for trusted internal data, or provide a validation/parser function when data needs runtime checking.
108
+
109
+ ```ts
110
+ function parseTodo(record: Record<string, any>): Todo {
111
+ if (typeof record.userId !== 'string') throw new Error('Invalid userId');
112
+ if (typeof record.id !== 'string') throw new Error('Invalid id');
113
+ if (record.status !== 'open' && record.status !== 'done') throw new Error('Invalid status');
114
+ return record as Todo;
115
+ }
116
+ ```
117
+
118
+ ### Save, get, delete
119
+
120
+ ```ts
121
+ await todos.save({
122
+ userId: 'u1',
123
+ id: 't1',
124
+ status: 'open',
125
+ createdAt: new Date().toISOString(),
126
+ title: 'Write docs',
127
+ }, { onlyIfNotExists: true });
128
+
129
+ const todo = await todos.get({ userId: 'u1', id: 't1' });
130
+ await todos.delete({ userId: 'u1', id: 't1' });
131
+ ```
132
+
133
+ ### Query
134
+
135
+ `query()` targets a primary key or named index. Index names are derived from fields: partition-only `status`; partition+sort `status-createdAt`; primary key `userId-id`.
136
+
137
+ ```ts
138
+ for await (const todo of todos.query({
139
+ by: 'userId-id',
140
+ where: { userId: 'u1' },
141
+ filter: { status: { eq: 'open' } },
142
+ })) {
143
+ console.log(todo.title);
144
+ }
145
+ ```
146
+
147
+ ```ts
148
+ for await (const todo of todos.query({
149
+ by: 'status-createdAt',
150
+ where: {
151
+ status: 'open',
152
+ createdAt: { beginsWith: '2026-07' },
153
+ },
154
+ })) {
155
+ console.log(todo.title);
156
+ }
157
+ ```
158
+
159
+ ### Filters
160
+
161
+ Filters support field comparisons and composition:
162
+
163
+ ```ts
164
+ const filter = {
165
+ and: [
166
+ { status: { eq: 'open' } },
167
+ { createdAt: { between: ['2026-07-01', '2026-08-01'] } },
168
+ { not: { title: { beginsWith: '[archived]' } } },
169
+ ],
170
+ };
171
+ ```
172
+
173
+ Supported operators: `eq`, `ne`, `gt`, `ge`, `lt`, `le`, `between`, `beginsWith`, `and`, `or`, `not`.
174
+
175
+ ### Design guidance
176
+
177
+ - Choose partition keys carefully. Changing key definitions in production can be destructive for some providers.
178
+ - Prefer high-cardinality, non-sequential partition keys for scalable distribution.
179
+ - Use sort keys for ordered/range access within a partition.
180
+ - Use indexes for alternate access patterns.
181
+ - Avoid `scan()` for large production tables unless you expect the cost.
182
+ - Local development may implement queries as scans; production overrides should map to provider-native queries/indexes where possible.
@@ -0,0 +1,179 @@
1
+ # Invocation and runtime resources
2
+
3
+ ## Endpoint
4
+
5
+ Most app/API code should not define or call raw HTTP routes. WireJS APIs are normally imported like modules from client, SSR, and SSG code; the framework handles the server call protocol invisibly while preserving TypeScript types.
6
+
7
+ Use `Endpoint` only when you intentionally need a real HTTP path outside that WireJS module protocol. Common reasons include:
8
+
9
+ - webhooks and third-party callbacks;
10
+ - OIDC/provider callback paths;
11
+ - custom admin/settings portals;
12
+ - compatibility routes for callers that cannot import the WireJS API package;
13
+ - serving non-API content from an API module, such as generated images, files, feeds, or health-check text.
14
+
15
+ `Endpoint` registers an HTTP-accessible handler outside the normal generated API method surface.
16
+
17
+ ```ts
18
+ import { Endpoint } from 'wirejs-resources';
19
+
20
+ new Endpoint('webhooks', 'stripe', {
21
+ path: '/webhooks/stripe',
22
+ description: 'Stripe webhook receiver',
23
+ handle: async context => {
24
+ const signature = context.requestHeaders['stripe-signature'];
25
+ const body = context.requestBody;
26
+
27
+ context.responseHeaders['content-type'] = 'text/plain; charset=utf-8';
28
+ return 'ok';
29
+ },
30
+ });
31
+ ```
32
+
33
+ Options:
34
+
35
+ - `path` — optional route. If omitted, the endpoint path is derived from `absoluteId`.
36
+ - `handle(context)` — request handler. Current endpoint handlers return strings/promises of strings.
37
+ - `description` — human-readable purpose.
38
+
39
+ Wildcard-style paths may end in `%` for hosting layers that support pattern matching.
40
+
41
+ ```ts
42
+ new Endpoint('files', 'public-prefix', {
43
+ path: '/public/%',
44
+ handle: async context => `Requested ${context.location.pathname}`,
45
+ });
46
+ ```
47
+
48
+ Example generated image endpoint:
49
+
50
+ ```ts
51
+ new Endpoint('images', 'og-card', {
52
+ path: '/og-card.svg',
53
+ description: 'Generate an OpenGraph image.',
54
+ handle: async context => {
55
+ const title = context.location.searchParams.get('title') ?? 'Untitled';
56
+ context.responseHeaders['content-type'] = 'image/svg+xml; charset=utf-8';
57
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630">
58
+ <rect width="1200" height="630" fill="#111" />
59
+ <text x="80" y="320" fill="white" font-size="72">${title}</text>
60
+ </svg>`;
61
+ },
62
+ });
63
+ ```
64
+
65
+ `Endpoint.list()` and `Endpoint.get(key)` expose registered endpoints for hosting/deployment discovery.
66
+
67
+ `determineUrl()` combines the endpoint path with the `wirejs/endpoint-origin` setting. This is useful when registering webhook URLs with external services.
68
+
69
+ ```ts
70
+ const url = await endpoint.determineUrl();
71
+ ```
72
+
73
+ ## BackgroundJob
74
+
75
+ `BackgroundJob` schedules work that should continue after the current request/action has scheduled it.
76
+
77
+ ```ts
78
+ import { BackgroundJob } from 'wirejs-resources';
79
+
80
+ export const sendReceipt = new BackgroundJob('orders', 'send-receipt', {
81
+ handler: async (orderId: string) => {
82
+ // Recreate clients/resources inside the handler as needed.
83
+ // Send email, call external APIs, update tables, etc.
84
+ },
85
+ });
86
+
87
+ await sendReceipt.start('order-123');
88
+ ```
89
+
90
+ Important behavior:
91
+
92
+ - `start()` resolves when the job is scheduled, not when the handler completes.
93
+ - Arguments are JSON-serialized/deserialized before local execution to mimic cloud behavior.
94
+ - Handlers may run in a different process/environment from the caller.
95
+ - Do not depend on request-local state, closures, globals, or in-memory caches unless the handler recreates them safely.
96
+ - Local development currently does not enforce timeouts.
97
+
98
+ Good pattern:
99
+
100
+ ```ts
101
+ const resizeAvatar = new BackgroundJob('users', 'resize-avatar', {
102
+ handler: async (userId: string, avatarFile: string) => {
103
+ const files = createUserFileService(userId);
104
+ const original = await files.read(avatarFile);
105
+ // resize and write output
106
+ },
107
+ });
108
+ ```
109
+
110
+ Avoid:
111
+
112
+ ```ts
113
+ let currentRequestUser: string | undefined;
114
+
115
+ const job = new BackgroundJob('bad', 'closure-state', {
116
+ // This may not be the same process or value when the job runs.
117
+ handler: async () => console.log(currentRequestUser),
118
+ });
119
+ ```
120
+
121
+ ## CronJob
122
+
123
+ `CronJob` registers recurring work using a cron expression.
124
+
125
+ ```ts
126
+ import { CronJob } from 'wirejs-resources';
127
+
128
+ new CronJob('maintenance', 'nightly-cleanup', {
129
+ schedule: '0 3 * * *',
130
+ handler: async () => {
131
+ // delete expired sessions, compact data, sync reports, etc.
132
+ },
133
+ });
134
+ ```
135
+
136
+ Create cron jobs at module top level. Deployment adapters discover registered jobs and map them to provider schedulers. Dynamically creating cron jobs inside request handlers is not supported as a deployment model.
137
+
138
+ A `CronJob` can invoke a `BackgroundJob` if you want the same work to be triggerable manually and on a schedule:
139
+
140
+ ```ts
141
+ const syncReports = new BackgroundJob('reports', 'sync', {
142
+ handler: async () => {
143
+ // sync reports
144
+ },
145
+ });
146
+
147
+ new CronJob('reports', 'hourly-sync', {
148
+ schedule: '0 * * * *',
149
+ handler: async () => syncReports.start(),
150
+ });
151
+ ```
152
+
153
+ ## SystemAttribute
154
+
155
+ `SystemAttribute` describes runtime/deployment metadata. Hosting layers can attach runtime attributes to `Context`; app code can also define static attributes.
156
+
157
+ Use cases:
158
+
159
+ - expose deployment type;
160
+ - expose local/network/public origins;
161
+ - expose provider capabilities;
162
+ - provide diagnostics in admin screens.
163
+
164
+ ```ts
165
+ import { SystemAttribute } from 'wirejs-resources';
166
+
167
+ new SystemAttribute('app', 'support-email', {
168
+ description: 'Support contact displayed in diagnostics.',
169
+ value: 'support@example.com',
170
+ });
171
+ ```
172
+
173
+ Reading from context:
174
+
175
+ ```ts
176
+ for (const attr of context.systemInfo) {
177
+ console.log(attr.absoluteId, attr.value);
178
+ }
179
+ ```
@@ -0,0 +1,361 @@
1
+ # Services
2
+
3
+ Services are resource-scoped abstractions for capabilities that may have different local and production implementations.
4
+
5
+ ## FileService
6
+
7
+ `FileService` is resource-scoped file/blob-like storage.
8
+
9
+ ```ts
10
+ import { FileService, Resource } from 'wirejs-resources';
11
+
12
+ const uploadsScope = new Resource('app', 'uploads');
13
+ const files = new FileService(uploadsScope, 'images');
14
+
15
+ await files.write('avatar.txt', 'hello');
16
+ const text = await files.read('avatar.txt');
17
+
18
+ for await (const filename of files.list()) {
19
+ console.log(filename);
20
+ }
21
+
22
+ await files.delete('avatar.txt');
23
+ ```
24
+
25
+ Default local behavior writes under:
26
+
27
+ ```text
28
+ temp/wirejs-services/<resource-id>/...
29
+ ```
30
+
31
+ Use `onlyIfNotExists` to avoid overwriting existing files:
32
+
33
+ ```ts
34
+ await files.write('lock.json', '{}', { onlyIfNotExists: true });
35
+ ```
36
+
37
+ Deployment overrides can map `FileService` to object stores, buckets, or provider file/blob services.
38
+
39
+ ## AuthenticationService
40
+
41
+ `AuthenticationService` is the authentication abstraction used by app/API code to inspect and manage auth state. The local implementation provides username/password signup/signin/change-password/signout backed by `FileService` plus signed-cookie session state. It can also be configured with OIDC providers.
42
+
43
+ Construct it at module scope, then expose its context-wrapped API from your API module:
44
+
45
+ ```ts
46
+ import { AuthenticationService } from 'wirejs-resources';
47
+
48
+ export const authService = new AuthenticationService('app', 'auth', {
49
+ keepalive: true,
50
+ cookie: 'identity',
51
+ });
52
+
53
+ export const auth = authService.buildApi();
54
+ ```
55
+
56
+ The API returned by `buildApi()` includes:
57
+
58
+ - `getState(context)` — returns an `AuthenticationMachineState` for UI state machines.
59
+ - `setState(context, input)` — performs actions such as `signin`, `signup`, `signout`, `changepassword`, or OIDC actions.
60
+ - `getCurrentUser(context)` — returns the current user or `null`.
61
+ - `requireCurrentUser(context)` — returns the current user or throws `Unauthorized.`.
62
+
63
+ Example endpoint-style use:
64
+
65
+ ```ts
66
+ const user = await auth.requireCurrentUser(context);
67
+ return `Hello ${user.displayName}`;
68
+ ```
69
+
70
+ Machine-state example:
71
+
72
+ ```ts
73
+ const state = await auth.getState(context);
74
+
75
+ if (state.state === 'unauthenticated') {
76
+ console.log(Object.keys(state.actions)); // signin, signup, OIDC actions, ...
77
+ }
78
+
79
+ const result = await auth.setState(context, {
80
+ key: 'signin',
81
+ inputs: {
82
+ username: 'alice',
83
+ password: 'correct horse battery staple',
84
+ },
85
+ });
86
+ ```
87
+
88
+ Constructor options include:
89
+
90
+ - `keepalive` — refreshes authenticated cookie state while active.
91
+ - `cookie` — cookie-name suffix; the final cookie name is scope-prefixed.
92
+ - `oidcProviders` — provider definitions for OIDC signin flows.
93
+
94
+ OIDC provider presets are exported for common providers:
95
+
96
+ ```ts
97
+ import {
98
+ AuthenticationService,
99
+ GoogleOIDCProvider,
100
+ GitHubOIDCProvider,
101
+ } from 'wirejs-resources';
102
+
103
+ export const authService = new AuthenticationService('app', 'auth', {
104
+ oidcProviders: [
105
+ GoogleOIDCProvider,
106
+ GitHubOIDCProvider,
107
+ ],
108
+ });
109
+ ```
110
+
111
+ Available presets include Google, Facebook, GitHub, and Apple. Provider flows generally also require client IDs/secrets or deployment-specific configuration; check the active deployment package and installed TypeScript types for the exact wiring.
112
+
113
+ Deployment providers may back this with hosted identity, OIDC flows, signed cookies, or other mechanisms.
114
+
115
+ ## RealtimeService
116
+
117
+ `RealtimeService<T>` provides channel-based server-to-client message streams. The local implementation uses WebSockets and signed channel tokens.
118
+
119
+ ```ts
120
+ import { RealtimeService } from 'wirejs-resources';
121
+
122
+ type TodoEvent =
123
+ | { type: 'created'; id: string; title: string }
124
+ | { type: 'completed'; id: string };
125
+
126
+ const realtime = new RealtimeService<TodoEvent>('app', 'todo-events');
127
+ ```
128
+
129
+ Publishing from server/API code:
130
+
131
+ ```ts
132
+ await realtime.publish('user-u1', [
133
+ { type: 'created', id: 't1', title: 'Write docs' },
134
+ ]);
135
+ ```
136
+
137
+ Creating a stream descriptor for the client:
138
+
139
+ ```ts
140
+ const stream = await realtime.getStream(context, 'user-u1');
141
+ ```
142
+
143
+ The returned value is typed as `MessageStream<T>`, but in practice it carries metadata that the WireJS client/runtime can use to subscribe to the websocket channel. Channel names are intentionally restricted for provider portability: up to five slash-separated segments; each segment is 1-50 alphanumeric/dash characters.
144
+
145
+ Use cases:
146
+
147
+ - server-to-client notifications;
148
+ - table/resource update broadcasts;
149
+ - collaborative UI updates;
150
+ - low-volume realtime app events.
151
+
152
+ Deployment overrides can map realtime channels to provider websocket/pubsub infrastructure.
153
+
154
+ ## LLM
155
+
156
+ `LLM` represents a large-language-model endpoint/configuration with model preferences, optional system prompt, target context size, and tool schemas.
157
+
158
+ Conversation messages use OpenAI-like roles:
159
+
160
+ - `user`
161
+ - `assistant`
162
+ - `tool`
163
+
164
+ ### One-shot responses
165
+
166
+ For a clean one-shot response, omit `onChunk` and await the returned assistant message:
167
+
168
+ ```ts
169
+ import { LLM } from 'wirejs-resources';
170
+
171
+ const assistant = new LLM('ai', 'assistant', {
172
+ models: ['llama3.1', 'gpt-4o-mini'],
173
+ systemPrompt: 'You answer concisely.',
174
+ });
175
+
176
+ const reply = await assistant.continueConversation({
177
+ history: [
178
+ { role: 'user', content: 'Summarize our refund policy in one paragraph.' },
179
+ ],
180
+ });
181
+
182
+ console.log(reply.content);
183
+ ```
184
+
185
+ ### Streaming responses
186
+
187
+ Use `onChunk` when you want to update UI progressively, persist partial output, or broadcast partial responses over realtime channels:
188
+
189
+ ```ts
190
+ const reply = await assistant.continueConversation({
191
+ history: [
192
+ { role: 'user', content: 'Where is my order?' },
193
+ ],
194
+ onChunk: chunk => {
195
+ if (chunk.message.role === 'assistant') {
196
+ console.log(chunk.message.content);
197
+ }
198
+ },
199
+ });
200
+
201
+ console.log('final:', reply.content);
202
+ ```
203
+
204
+ ### Background orchestration
205
+
206
+ `LLM` does not schedule background work by itself. `continueConversation()` runs in the current server execution. For chat-style UX, long tool loops, or request-independent processing, start the workflow from an app-owned `BackgroundJob` or equivalent API orchestration and store/broadcast progress yourself.
207
+
208
+ ```ts
209
+ import { BackgroundJob, LLM, RealtimeService } from 'wirejs-resources';
210
+
211
+ const assistant = new LLM('ai', 'assistant', {
212
+ models: ['llama3.1'],
213
+ });
214
+
215
+ const updates = new RealtimeService<{ room: string; text: string }>('ai', 'updates');
216
+
217
+ export const respondInBackground = new BackgroundJob('ai', 'respond', {
218
+ handler: async (room: string, prompt: string) => {
219
+ const reply = await assistant.continueConversation({
220
+ history: [{ role: 'user', content: prompt }],
221
+ onChunk: async chunk => {
222
+ if (chunk.message.role === 'assistant') {
223
+ await updates.publish(room, [{ room, text: chunk.message.content }]);
224
+ }
225
+ },
226
+ });
227
+
228
+ // Persist final reply in app storage here if needed.
229
+ await updates.publish(room, [{ room, text: reply.content }]);
230
+ },
231
+ });
232
+
233
+ // API/endpoint code can schedule and return quickly:
234
+ await respondInBackground.start('room-1', 'Write a short welcome message.');
235
+ ```
236
+
237
+ ### Tool calling
238
+
239
+ `LLM` provides model-side tool-calling support, not tool execution. The `ToolDefinition` exported by `wirejs-resources` describes tools to the model. Application code owns the tool execution loop: call the model, inspect `assistant.tool_calls`, execute matching server-side functions, append a `tool` message with results, then call the model again.
240
+
241
+ The generated app template contains a fuller chat/tool example under `api/apps/llm/`:
242
+
243
+ - `tools.ts` defines tool schemas plus `execute()` functions.
244
+ - `tooled-handler.ts` loops over `tool_calls`, runs matching tools, and appends `ToolMessage` results.
245
+ - `infra.ts` handles conversation persistence, streaming chunks, realtime status messages, and calls to `LLM.continueConversation()`.
246
+
247
+ Minimal tool-loop shape:
248
+
249
+ ```ts
250
+ import {
251
+ LLM,
252
+ ToolDefinition as BaseToolDefinition,
253
+ type LLMMessage,
254
+ type ToolMessage,
255
+ } from 'wirejs-resources';
256
+
257
+ type ExecutableTool = BaseToolDefinition & {
258
+ execute(args: Record<string, any>): Promise<unknown>;
259
+ };
260
+
261
+ const tools: ExecutableTool[] = [
262
+ {
263
+ name: 'lookup_order',
264
+ description: 'Look up an order by ID.',
265
+ parameters: {
266
+ type: 'object',
267
+ properties: {
268
+ orderId: { type: 'string', description: 'Order ID' },
269
+ },
270
+ required: ['orderId'],
271
+ },
272
+ async execute({ orderId }) {
273
+ // This runs on the server. Validate args and enforce authz here.
274
+ return { orderId, status: 'shipped' };
275
+ },
276
+ },
277
+ ];
278
+
279
+ const assistant = new LLM('ai', 'support-assistant', {
280
+ models: ['llama3.1', 'gpt-4o-mini'],
281
+ systemPrompt: 'You help users with account questions.',
282
+ targetContextSize: 16_384,
283
+ });
284
+
285
+ const history: LLMMessage[] = [
286
+ { role: 'user', content: 'Where is order 123?' },
287
+ ];
288
+
289
+ for (let remainingToolRounds = 5; remainingToolRounds > 0; remainingToolRounds--) {
290
+ const response = await assistant.continueConversation({
291
+ history,
292
+ tools,
293
+ });
294
+ history.push(response);
295
+
296
+ if (!response.tool_calls?.length) break;
297
+
298
+ const toolMessage: ToolMessage = { role: 'tool', content: [] };
299
+ for (const call of response.tool_calls) {
300
+ const tool = tools.find(t => t.name === call.function.name);
301
+ try {
302
+ if (!tool) throw new Error(`Unknown tool: ${call.function.name}`);
303
+ const result = await tool.execute(call.function.arguments);
304
+ toolMessage.content.push({
305
+ id: call.id || JSON.stringify([call.function.name, call.function.arguments]),
306
+ content: JSON.stringify(result, null, 2),
307
+ });
308
+ } catch (error) {
309
+ toolMessage.content.push({
310
+ id: call.id || JSON.stringify([call.function.name, call.function.arguments]),
311
+ content: String(error),
312
+ isError: true,
313
+ });
314
+ }
315
+ }
316
+
317
+ history.push(toolMessage);
318
+ }
319
+ ```
320
+
321
+ Provider availability and exact model behavior depend on the active implementation.
322
+
323
+ ## EmailSender
324
+
325
+ `EmailSender` sends email through either a specific sender address or a sender domain. The local implementation writes JSON files to `.outbox/`.
326
+
327
+ Specific address:
328
+
329
+ ```ts
330
+ import { EmailSender } from 'wirejs-resources';
331
+
332
+ const mailer = new EmailSender('notifications', 'main', {
333
+ from: 'hello@example.com',
334
+ });
335
+
336
+ await mailer.send({
337
+ to: 'user@example.com',
338
+ subject: 'Welcome',
339
+ body: 'Thanks for trying WireJS.',
340
+ html: '<p>Thanks for trying WireJS.</p>',
341
+ });
342
+ ```
343
+
344
+ Domain sender:
345
+
346
+ ```ts
347
+ const domainMailer = new EmailSender('notifications', 'domain', {
348
+ fromDomain: 'example.com',
349
+ });
350
+
351
+ await domainMailer.send({
352
+ from: 'receipts@example.com',
353
+ to: ['buyer@example.net'],
354
+ subject: 'Receipt',
355
+ body: 'Your receipt is attached.',
356
+ });
357
+ ```
358
+
359
+ When using `fromDomain`, each message must provide `from`, and it must end with the configured domain. This lets deployments verify one domain while still allowing multiple sender addresses within that domain.
360
+
361
+ Local `.outbox` files are useful for development and tests; production overrides should map to provider email services.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wirejs-resources",
3
- "version": "0.1.191",
3
+ "version": "0.1.192",
4
4
  "description": "Basic services and server-side resources for wirejs apps",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -44,6 +44,7 @@
44
44
  "files": [
45
45
  "package.json",
46
46
  "README.md",
47
+ "docs/*",
47
48
  "dist/*"
48
49
  ]
49
50
  }