wirejs-resources 0.1.166 → 0.1.168
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/adapters/cookie-jar.d.ts +2 -0
- package/dist/adapters/signed-cookie.js +2 -1
- package/dist/hosting/client.d.ts +1 -0
- package/dist/hosting/client.js +68 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/internal/client.d.ts +1 -0
- package/dist/internal/client.js +68 -0
- package/dist/resources/secret.d.ts +9 -3
- package/dist/resources/secret.js +28 -18
- package/dist/services/authentication.d.ts +7 -2
- package/dist/services/authentication.js +27 -8
- package/dist/services/llm.d.ts +5 -6
- package/dist/services/llm.js +5 -6
- package/dist/services/oidc-providers.d.ts +15 -0
- package/dist/services/oidc-providers.js +63 -0
- package/dist/services/oidc.d.ts +16 -0
- package/dist/services/oidc.js +212 -0
- package/dist/setup/index.d.ts +1 -0
- package/dist/setup/index.js +27 -0
- package/dist/types/index.d.ts +61 -0
- package/dist/types/resources/distributed-table.d.ts +6 -0
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ export type Cookie = {
|
|
|
4
4
|
httpOnly?: boolean;
|
|
5
5
|
secure?: boolean;
|
|
6
6
|
maxAge?: number;
|
|
7
|
+
path?: string;
|
|
7
8
|
};
|
|
8
9
|
export declare class CookieJar {
|
|
9
10
|
#private;
|
|
@@ -18,6 +19,7 @@ export declare class CookieJar {
|
|
|
18
19
|
httpOnly?: boolean;
|
|
19
20
|
secure?: boolean;
|
|
20
21
|
maxAge?: number;
|
|
22
|
+
path?: string;
|
|
21
23
|
} | undefined;
|
|
22
24
|
delete(name: string): void;
|
|
23
25
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function apiTree(INTERNAL_API_URL: string, path?: string[]): () => void;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
async function callApi(INTERNAL_API_URL, method, ...args) {
|
|
2
|
+
function isNode() {
|
|
3
|
+
return typeof args[0]?.cookies?.getAll === 'function';
|
|
4
|
+
}
|
|
5
|
+
function apiUrl() {
|
|
6
|
+
if (isNode()) {
|
|
7
|
+
return INTERNAL_API_URL;
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
return "/api";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
let cookieHeader = {};
|
|
14
|
+
if (isNode()) {
|
|
15
|
+
const context = args[0];
|
|
16
|
+
const cookies = context.cookies.getAll();
|
|
17
|
+
cookieHeader = typeof cookies === 'object'
|
|
18
|
+
? {
|
|
19
|
+
Cookie: Object.entries(cookies).map(kv => kv.join('=')).join('; ')
|
|
20
|
+
}
|
|
21
|
+
: {};
|
|
22
|
+
}
|
|
23
|
+
const response = await fetch(apiUrl(), {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
...cookieHeader
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify([{ method, args: [...args] }]),
|
|
30
|
+
});
|
|
31
|
+
const body = await response.json();
|
|
32
|
+
if (isNode()) {
|
|
33
|
+
const context = args[0];
|
|
34
|
+
for (const c of response.headers.getSetCookie()) {
|
|
35
|
+
const parts = c.split(';').map(p => p.trim());
|
|
36
|
+
const flags = parts.slice(1);
|
|
37
|
+
const [name, value] = parts[0].split('=').map(decodeURIComponent);
|
|
38
|
+
const httpOnly = flags.includes('HttpOnly');
|
|
39
|
+
const secure = flags.includes('Secure');
|
|
40
|
+
const maxAgePart = flags.find(f => f.startsWith('Max-Age='))?.split('=')[1];
|
|
41
|
+
context.cookies.set({
|
|
42
|
+
name,
|
|
43
|
+
value,
|
|
44
|
+
httpOnly,
|
|
45
|
+
secure,
|
|
46
|
+
maxAge: maxAgePart ? parseInt(maxAgePart) : undefined
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const error = body[0].error;
|
|
51
|
+
if (error) {
|
|
52
|
+
throw new Error(error);
|
|
53
|
+
}
|
|
54
|
+
const value = body[0].data;
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
;
|
|
58
|
+
export function apiTree(INTERNAL_API_URL, path = []) {
|
|
59
|
+
return new Proxy(function () { }, {
|
|
60
|
+
apply(_target, _thisArg, args) {
|
|
61
|
+
return callApi(INTERNAL_API_URL, path, ...args);
|
|
62
|
+
},
|
|
63
|
+
get(_target, prop) {
|
|
64
|
+
return apiTree(INTERNAL_API_URL, [...path, prop]);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { FileService } from './services/file.js';
|
|
2
2
|
export { AuthenticationService } from './services/authentication.js';
|
|
3
|
+
export * from './services/oidc-providers.js';
|
|
3
4
|
export { CookieJar } from './adapters/cookie-jar.js';
|
|
4
5
|
export { SignedCookie } from './adapters/signed-cookie.js';
|
|
5
6
|
export { withContext, requiresContext, Context, ContextWrapped } from './adapters/context.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { FileService } from './services/file.js';
|
|
2
2
|
export { AuthenticationService } from './services/authentication.js';
|
|
3
|
+
export * from './services/oidc-providers.js';
|
|
3
4
|
export { CookieJar } from './adapters/cookie-jar.js';
|
|
4
5
|
export { SignedCookie } from './adapters/signed-cookie.js';
|
|
5
6
|
export { withContext, requiresContext, Context } from './adapters/context.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function apiTree(INTERNAL_API_URL: string, path?: string[]): () => void;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
async function callApi(INTERNAL_API_URL, method, ...args) {
|
|
2
|
+
function isNode() {
|
|
3
|
+
return typeof args[0]?.cookies?.getAll === 'function';
|
|
4
|
+
}
|
|
5
|
+
function apiUrl() {
|
|
6
|
+
if (isNode()) {
|
|
7
|
+
return INTERNAL_API_URL;
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
return "/api";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
let cookieHeader = {};
|
|
14
|
+
if (isNode()) {
|
|
15
|
+
const context = args[0];
|
|
16
|
+
const cookies = context.cookies.getAll();
|
|
17
|
+
cookieHeader = typeof cookies === 'object'
|
|
18
|
+
? {
|
|
19
|
+
Cookie: Object.entries(cookies).map(kv => kv.join('=')).join('; ')
|
|
20
|
+
}
|
|
21
|
+
: {};
|
|
22
|
+
}
|
|
23
|
+
const response = await fetch(apiUrl(), {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
...cookieHeader
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify([{ method, args: [...args] }]),
|
|
30
|
+
});
|
|
31
|
+
const body = await response.json();
|
|
32
|
+
if (isNode()) {
|
|
33
|
+
const context = args[0];
|
|
34
|
+
for (const c of response.headers.getSetCookie()) {
|
|
35
|
+
const parts = c.split(';').map(p => p.trim());
|
|
36
|
+
const flags = parts.slice(1);
|
|
37
|
+
const [name, value] = parts[0].split('=').map(decodeURIComponent);
|
|
38
|
+
const httpOnly = flags.includes('HttpOnly');
|
|
39
|
+
const secure = flags.includes('Secure');
|
|
40
|
+
const maxAgePart = flags.find(f => f.startsWith('Max-Age='))?.split('=')[1];
|
|
41
|
+
context.cookies.set({
|
|
42
|
+
name,
|
|
43
|
+
value,
|
|
44
|
+
httpOnly,
|
|
45
|
+
secure,
|
|
46
|
+
maxAge: maxAgePart ? parseInt(maxAgePart) : undefined
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const error = body[0].error;
|
|
51
|
+
if (error) {
|
|
52
|
+
throw new Error(error);
|
|
53
|
+
}
|
|
54
|
+
const value = body[0].data;
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
;
|
|
58
|
+
export function apiTree(INTERNAL_API_URL, path = []) {
|
|
59
|
+
return new Proxy(function () { }, {
|
|
60
|
+
apply(_target, _thisArg, args) {
|
|
61
|
+
return callApi(INTERNAL_API_URL, path, ...args);
|
|
62
|
+
},
|
|
63
|
+
get(_target, prop) {
|
|
64
|
+
return apiTree(INTERNAL_API_URL, [...path, prop]);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
;
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { Resource } from '../resource.js';
|
|
2
2
|
export declare class Secret extends Resource {
|
|
3
|
-
#private;
|
|
4
3
|
constructor(scope: Resource | string, id: string);
|
|
5
|
-
read(): Promise<
|
|
6
|
-
write(
|
|
4
|
+
read(): Promise<string>;
|
|
5
|
+
write(value: string, options?: {
|
|
6
|
+
onlyIfNotExists?: boolean;
|
|
7
|
+
}): Promise<void | undefined>;
|
|
8
|
+
scan(): AsyncGenerator<never, AsyncGenerator<{
|
|
9
|
+
key: string;
|
|
10
|
+
value: string;
|
|
11
|
+
}, any, any> | undefined, unknown>;
|
|
12
|
+
isAlreadyExistsError(error: any): boolean | undefined;
|
|
7
13
|
}
|
package/dist/resources/secret.js
CHANGED
|
@@ -1,28 +1,38 @@
|
|
|
1
|
-
import crypto from 'crypto';
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
2
|
import { Resource } from '../resource.js';
|
|
3
|
-
import {
|
|
3
|
+
import { KeyValueStore } from './key-value-store.js';
|
|
4
4
|
import { overrides } from '../overrides.js';
|
|
5
|
-
|
|
5
|
+
let secrets;
|
|
6
6
|
export class Secret extends Resource {
|
|
7
|
-
#fileService;
|
|
8
|
-
#initPromise;
|
|
9
7
|
constructor(scope, id) {
|
|
10
8
|
super(scope, id);
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
#initialize() {
|
|
14
|
-
this.#initPromise = this.#initPromise || this.#fileService.write(FILENAME, JSON.stringify(crypto.randomBytes(64).toString('base64url')), { onlyIfNotExists: true }).catch(error => {
|
|
15
|
-
if (!this.#fileService.isAlreadyExistsError(error))
|
|
16
|
-
throw error;
|
|
17
|
-
});
|
|
18
|
-
return this.#initPromise;
|
|
9
|
+
secrets = new (overrides.KeyValueStore || KeyValueStore)('wirejs', 'secrets');
|
|
19
10
|
}
|
|
20
11
|
async read() {
|
|
21
|
-
await this
|
|
22
|
-
|
|
12
|
+
const existingValue = await secrets?.get(this.absoluteId);
|
|
13
|
+
if (!existingValue) {
|
|
14
|
+
try {
|
|
15
|
+
const initValue = crypto.randomBytes(64).toString('base64url');
|
|
16
|
+
await this.write(initValue, { onlyIfNotExists: true });
|
|
17
|
+
return initValue;
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (!this.isAlreadyExistsError(error))
|
|
21
|
+
throw error;
|
|
22
|
+
return this.read();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
return existingValue;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async write(value, options) {
|
|
30
|
+
return secrets?.set(this.absoluteId, value, { onlyIfNotExists: options?.onlyIfNotExists });
|
|
31
|
+
}
|
|
32
|
+
async *scan() {
|
|
33
|
+
return secrets?.scan();
|
|
23
34
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
await this.#fileService.write(FILENAME, JSON.stringify(data));
|
|
35
|
+
isAlreadyExistsError(error) {
|
|
36
|
+
return secrets?.isAlreadyExistsError(error);
|
|
27
37
|
}
|
|
28
38
|
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { Resource } from '../resource.js';
|
|
2
2
|
import { ContextWrapped } from '../adapters/context.js';
|
|
3
3
|
import type { CookieJar } from '../adapters/cookie-jar.js';
|
|
4
|
-
import type { User, AuthenticationError, AuthenticationMachineInput, AuthenticationMachineState, AuthenticationServiceOptions } from '../types/index.js';
|
|
4
|
+
import type { User, AuthenticationError, AuthenticationMachineAction, AuthenticationMachineInput, AuthenticationMachineState, AuthenticationServiceOptions } from '../types/index.js';
|
|
5
5
|
export declare class AuthenticationService extends Resource {
|
|
6
6
|
#private;
|
|
7
|
-
constructor(scope: Resource | string, id: string, { duration, keepalive, cookie }?: AuthenticationServiceOptions);
|
|
7
|
+
constructor(scope: Resource | string, id: string, { duration, keepalive, cookie, oidcProviders }?: AuthenticationServiceOptions);
|
|
8
8
|
private getState;
|
|
9
|
+
/**
|
|
10
|
+
* Returns machine actions for every configured and enabled OIDC provider.
|
|
11
|
+
* Subclasses should merge this into their own `getMachineState` result.
|
|
12
|
+
*/
|
|
13
|
+
protected getOidcMachineActions(): Promise<Record<string, AuthenticationMachineAction>>;
|
|
9
14
|
getMachineState(cookies: CookieJar): Promise<AuthenticationMachineState>;
|
|
10
15
|
private setState;
|
|
11
16
|
missingFieldErrors(input: Record<string, string | number | boolean>, fields: string[]): AuthenticationError[] | undefined;
|
|
@@ -2,9 +2,12 @@ import { scrypt, randomBytes, randomUUID } from 'crypto';
|
|
|
2
2
|
import { Resource } from '../resource.js';
|
|
3
3
|
import { FileService } from './file.js';
|
|
4
4
|
import { Setting } from '../resources/setting.js';
|
|
5
|
+
import { Endpoint } from '../resources/endpoint.js';
|
|
6
|
+
import { DistributedTable } from '../resources/distributed-table.js';
|
|
5
7
|
import { SignedCookie } from '../adapters/signed-cookie.js';
|
|
6
8
|
import { withContext } from '../adapters/context.js';
|
|
7
9
|
import { overrides } from '../overrides.js';
|
|
10
|
+
import { OIDCManager } from './oidc.js';
|
|
8
11
|
function newId() {
|
|
9
12
|
return randomUUID();
|
|
10
13
|
}
|
|
@@ -184,7 +187,8 @@ export class AuthenticationService extends Resource {
|
|
|
184
187
|
#keepalive;
|
|
185
188
|
#users;
|
|
186
189
|
#cookie;
|
|
187
|
-
|
|
190
|
+
#oidcManager;
|
|
191
|
+
constructor(scope, id, { duration, keepalive, cookie, oidcProviders } = {}) {
|
|
188
192
|
super(scope, id);
|
|
189
193
|
this.#keepalive = !!keepalive;
|
|
190
194
|
const SettingCtor = overrides.Setting || Setting;
|
|
@@ -200,6 +204,13 @@ export class AuthenticationService extends Resource {
|
|
|
200
204
|
// exclusively for development purposes.
|
|
201
205
|
{ maxAge: ONE_WEEK, secure: false });
|
|
202
206
|
this.#users = new UserStore(fileService);
|
|
207
|
+
if (oidcProviders && oidcProviders.length > 0) {
|
|
208
|
+
this.#oidcManager = new OIDCManager(this, oidcProviders, this.#cookie, {
|
|
209
|
+
Setting: SettingCtor,
|
|
210
|
+
Endpoint: overrides.Endpoint || Endpoint,
|
|
211
|
+
DistributedTable: overrides.DistributedTable || DistributedTable,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
203
214
|
}
|
|
204
215
|
async getState(cookies) {
|
|
205
216
|
const cookieState = await this.#cookie.read(cookies);
|
|
@@ -208,25 +219,33 @@ export class AuthenticationService extends Resource {
|
|
|
208
219
|
user: undefined
|
|
209
220
|
};
|
|
210
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Returns machine actions for every configured and enabled OIDC provider.
|
|
224
|
+
* Subclasses should merge this into their own `getMachineState` result.
|
|
225
|
+
*/
|
|
226
|
+
async getOidcMachineActions() {
|
|
227
|
+
return this.#oidcManager?.getMachineActions() ?? {};
|
|
228
|
+
}
|
|
211
229
|
async getMachineState(cookies) {
|
|
212
230
|
const state = await this.getState(cookies);
|
|
213
231
|
if (state.state === 'authenticated') {
|
|
214
232
|
if (this.#keepalive)
|
|
215
233
|
this.setState(cookies, state.user);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
actions
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
};
|
|
234
|
+
const actions = {};
|
|
235
|
+
if (!state.user.oidcProvider) {
|
|
236
|
+
actions.changepassword = machineAction('changepassword');
|
|
237
|
+
}
|
|
238
|
+
actions.signout = machineAction('signout');
|
|
239
|
+
return { ...state, actions };
|
|
223
240
|
}
|
|
224
241
|
else {
|
|
242
|
+
const oidcActions = await this.getOidcMachineActions();
|
|
225
243
|
return {
|
|
226
244
|
...state,
|
|
227
245
|
actions: {
|
|
228
246
|
signin: machineAction('signin'),
|
|
229
247
|
signup: machineAction('signup'),
|
|
248
|
+
...oidcActions,
|
|
230
249
|
}
|
|
231
250
|
};
|
|
232
251
|
}
|
package/dist/services/llm.d.ts
CHANGED
|
@@ -30,12 +30,11 @@ export type AssistantMessage = {
|
|
|
30
30
|
export type ToolMessage = {
|
|
31
31
|
/** Always 'tool' for tool execution results */
|
|
32
32
|
role: 'tool';
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
tool_call_id: string;
|
|
33
|
+
content: {
|
|
34
|
+
id: string;
|
|
35
|
+
content: string;
|
|
36
|
+
isError?: boolean;
|
|
37
|
+
}[];
|
|
39
38
|
};
|
|
40
39
|
/** Union of all possible message types in conversation history */
|
|
41
40
|
export type LLMMessage = UserMessage | AssistantMessage | ToolMessage;
|
package/dist/services/llm.js
CHANGED
|
@@ -141,7 +141,10 @@ export class LLM extends Resource {
|
|
|
141
141
|
content: finalSystemPrompt
|
|
142
142
|
}
|
|
143
143
|
] : []),
|
|
144
|
-
...history
|
|
144
|
+
...history.map(m => m.role === 'tool' ? {
|
|
145
|
+
role: 'tool',
|
|
146
|
+
content: JSON.stringify(m.content, null, 2)
|
|
147
|
+
} : m)
|
|
145
148
|
],
|
|
146
149
|
stream,
|
|
147
150
|
...(ollamaTools.length > 0 ? { tools: ollamaTools } : {}),
|
|
@@ -166,11 +169,7 @@ export class LLM extends Resource {
|
|
|
166
169
|
return message;
|
|
167
170
|
}
|
|
168
171
|
else {
|
|
169
|
-
|
|
170
|
-
return {
|
|
171
|
-
role: 'assistant',
|
|
172
|
-
content: message.content
|
|
173
|
-
};
|
|
172
|
+
throw new Error(`Unexpected message format: ${message}`);
|
|
174
173
|
}
|
|
175
174
|
}
|
|
176
175
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { OIDCProviderConfig } from '../types/index.js';
|
|
2
|
+
export type { OIDCProviderConfig };
|
|
3
|
+
/** Google OIDC provider configuration. */
|
|
4
|
+
export declare const GoogleOIDCProvider: OIDCProviderConfig;
|
|
5
|
+
/** Facebook OIDC provider configuration. */
|
|
6
|
+
export declare const FacebookOIDCProvider: OIDCProviderConfig;
|
|
7
|
+
/**
|
|
8
|
+
* GitHub OAuth provider configuration.
|
|
9
|
+
*
|
|
10
|
+
* Note: GitHub does not fully implement OIDC (no id_token is returned).
|
|
11
|
+
* User info is fetched from the GitHub API using the access token.
|
|
12
|
+
*/
|
|
13
|
+
export declare const GitHubOIDCProvider: OIDCProviderConfig;
|
|
14
|
+
/** Apple Sign In OIDC provider configuration. */
|
|
15
|
+
export declare const AppleOIDCProvider: OIDCProviderConfig;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Google OIDC provider configuration. */
|
|
2
|
+
export const GoogleOIDCProvider = {
|
|
3
|
+
id: 'google',
|
|
4
|
+
name: 'Google',
|
|
5
|
+
authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
6
|
+
tokenEndpoint: 'https://oauth2.googleapis.com/token',
|
|
7
|
+
jwksUri: 'https://www.googleapis.com/oauth2/v3/certs',
|
|
8
|
+
issuer: 'https://accounts.google.com',
|
|
9
|
+
scopes: ['openid', 'email', 'profile'],
|
|
10
|
+
buttonStyle: 'background: #fff; color: #3c4043; border: 1px solid #dadce0;',
|
|
11
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18">
|
|
12
|
+
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
|
13
|
+
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
|
14
|
+
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"/>
|
|
15
|
+
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
|
16
|
+
</svg>`,
|
|
17
|
+
};
|
|
18
|
+
/** Facebook OIDC provider configuration. */
|
|
19
|
+
export const FacebookOIDCProvider = {
|
|
20
|
+
id: 'facebook',
|
|
21
|
+
name: 'Facebook',
|
|
22
|
+
authorizationEndpoint: 'https://www.facebook.com/v18.0/dialog/oauth',
|
|
23
|
+
tokenEndpoint: 'https://graph.facebook.com/v18.0/oauth/access_token',
|
|
24
|
+
jwksUri: 'https://www.facebook.com/.well-known/oauth/openid/jwks/',
|
|
25
|
+
issuer: 'https://www.facebook.com',
|
|
26
|
+
scopes: ['openid', 'email', 'public_profile'],
|
|
27
|
+
buttonStyle: 'background: #1877F2; color: #fff; border: none;',
|
|
28
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="#fff">
|
|
29
|
+
<path d="M24 12.073C24 5.405 18.627 0 12 0S0 5.405 0 12.073c0 6.027 4.388 11.02 10.125 11.927v-8.437H7.078v-3.49h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.49h-2.796v8.437C19.612 23.093 24 18.1 24 12.073z"/>
|
|
30
|
+
</svg>`,
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* GitHub OAuth provider configuration.
|
|
34
|
+
*
|
|
35
|
+
* Note: GitHub does not fully implement OIDC (no id_token is returned).
|
|
36
|
+
* User info is fetched from the GitHub API using the access token.
|
|
37
|
+
*/
|
|
38
|
+
export const GitHubOIDCProvider = {
|
|
39
|
+
id: 'github',
|
|
40
|
+
name: 'GitHub',
|
|
41
|
+
authorizationEndpoint: 'https://github.com/login/oauth/authorize',
|
|
42
|
+
tokenEndpoint: 'https://github.com/login/oauth/access_token',
|
|
43
|
+
userInfoEndpoint: 'https://api.github.com/user',
|
|
44
|
+
scopes: ['read:user', 'user:email'],
|
|
45
|
+
buttonStyle: 'background: #24292e; color: #fff; border: none;',
|
|
46
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="#fff">
|
|
47
|
+
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
|
|
48
|
+
</svg>`,
|
|
49
|
+
};
|
|
50
|
+
/** Apple Sign In OIDC provider configuration. */
|
|
51
|
+
export const AppleOIDCProvider = {
|
|
52
|
+
id: 'apple',
|
|
53
|
+
name: 'Apple',
|
|
54
|
+
authorizationEndpoint: 'https://appleid.apple.com/auth/authorize',
|
|
55
|
+
tokenEndpoint: 'https://appleid.apple.com/auth/token',
|
|
56
|
+
jwksUri: 'https://appleid.apple.com/auth/keys',
|
|
57
|
+
issuer: 'https://appleid.apple.com',
|
|
58
|
+
scopes: ['openid', 'email', 'name'],
|
|
59
|
+
buttonStyle: 'background: #000; color: #fff; border: none;',
|
|
60
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="#fff">
|
|
61
|
+
<path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"/>
|
|
62
|
+
</svg>`,
|
|
63
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Endpoint } from '../resources/endpoint.js';
|
|
2
|
+
import { DistributedTable } from '../resources/distributed-table.js';
|
|
3
|
+
import { Setting } from '../resources/setting.js';
|
|
4
|
+
import type { Resource } from '../resource.js';
|
|
5
|
+
import type { SignedCookie } from '../adapters/signed-cookie.js';
|
|
6
|
+
import type { AuthenticationMachineAction, AuthenticationState, OIDCProviderConfig } from '../types/index.js';
|
|
7
|
+
export type OIDCCtors = {
|
|
8
|
+
Setting: typeof Setting;
|
|
9
|
+
Endpoint: typeof Endpoint;
|
|
10
|
+
DistributedTable: typeof DistributedTable;
|
|
11
|
+
};
|
|
12
|
+
export declare class OIDCManager {
|
|
13
|
+
#private;
|
|
14
|
+
constructor(scope: Resource | string, providers: OIDCProviderConfig[], cookie: SignedCookie<AuthenticationState>, ctors: OIDCCtors);
|
|
15
|
+
getMachineActions(): Promise<Record<string, AuthenticationMachineAction>>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { randomBytes, createHash } from 'crypto';
|
|
2
|
+
import * as jose from 'jose';
|
|
3
|
+
import { PassThruParser } from '../resources/distributed-table.js';
|
|
4
|
+
const PKCE_TTL_SECONDS = 10 * 60; // 10 minutes
|
|
5
|
+
function fallbackDisplayName(email) {
|
|
6
|
+
const emailUser = email.split('@')[0] || 'user';
|
|
7
|
+
return `${emailUser}-${randomBytes(3).toString('hex')}`;
|
|
8
|
+
}
|
|
9
|
+
async function extractIdentityFromIdToken(idToken, clientId, provider) {
|
|
10
|
+
let payload;
|
|
11
|
+
if (provider.jwksUri) {
|
|
12
|
+
const JWKS = jose.createRemoteJWKSet(new URL(provider.jwksUri));
|
|
13
|
+
const result = await jose.jwtVerify(idToken, JWKS, {
|
|
14
|
+
issuer: provider.issuer,
|
|
15
|
+
audience: clientId,
|
|
16
|
+
});
|
|
17
|
+
payload = result.payload;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
payload = jose.decodeJwt(idToken);
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
sub: String(payload.sub ?? ''),
|
|
24
|
+
email: String(payload.email ?? ''),
|
|
25
|
+
displayName: String(payload.name ?? payload.preferred_username ?? ''),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async function extractIdentityFromUserInfo(accessToken, userInfoEndpoint) {
|
|
29
|
+
const resp = await fetch(userInfoEndpoint, {
|
|
30
|
+
headers: {
|
|
31
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
32
|
+
'Accept': 'application/json',
|
|
33
|
+
'User-Agent': 'wirejs-resources',
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
if (!resp.ok) {
|
|
37
|
+
throw new Error(`UserInfo request failed: ${resp.status} ${resp.statusText}`);
|
|
38
|
+
}
|
|
39
|
+
const info = await resp.json();
|
|
40
|
+
return {
|
|
41
|
+
sub: String(info.id ?? info.sub ?? ''),
|
|
42
|
+
email: String(info.email ?? ''),
|
|
43
|
+
displayName: String(info.name ?? info.login ?? info.preferred_username ?? ''),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async function exchangeCode(provider, params) {
|
|
47
|
+
const resp = await fetch(provider.tokenEndpoint, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: {
|
|
50
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
51
|
+
'Accept': 'application/json',
|
|
52
|
+
},
|
|
53
|
+
body: new URLSearchParams({
|
|
54
|
+
grant_type: 'authorization_code',
|
|
55
|
+
code: params.code,
|
|
56
|
+
redirect_uri: params.redirectUri,
|
|
57
|
+
client_id: params.clientId,
|
|
58
|
+
client_secret: params.clientSecret,
|
|
59
|
+
code_verifier: params.codeVerifier,
|
|
60
|
+
}).toString(),
|
|
61
|
+
});
|
|
62
|
+
if (!resp.ok) {
|
|
63
|
+
throw new Error(`Token exchange failed: ${resp.status} ${resp.statusText}`);
|
|
64
|
+
}
|
|
65
|
+
return resp.json();
|
|
66
|
+
}
|
|
67
|
+
async function extractIdentity(tokens, clientId, provider) {
|
|
68
|
+
if (tokens.id_token) {
|
|
69
|
+
return extractIdentityFromIdToken(tokens.id_token, clientId, provider);
|
|
70
|
+
}
|
|
71
|
+
else if (tokens.access_token && provider.userInfoEndpoint) {
|
|
72
|
+
return extractIdentityFromUserInfo(tokens.access_token, provider.userInfoEndpoint);
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
export class OIDCManager {
|
|
77
|
+
#pkceTable;
|
|
78
|
+
#oidcUsersTable;
|
|
79
|
+
#providers = new Map();
|
|
80
|
+
#cookie;
|
|
81
|
+
constructor(scope, providers, cookie, ctors) {
|
|
82
|
+
this.#cookie = cookie;
|
|
83
|
+
this.#pkceTable = new ctors.DistributedTable(scope, 'oidc-pkce', {
|
|
84
|
+
parse: (PassThruParser),
|
|
85
|
+
key: { partition: { field: 'state', type: 'string' } },
|
|
86
|
+
ttlAttribute: 'ttl',
|
|
87
|
+
});
|
|
88
|
+
this.#oidcUsersTable = new ctors.DistributedTable(scope, 'oidc-users', {
|
|
89
|
+
parse: (PassThruParser),
|
|
90
|
+
key: {
|
|
91
|
+
partition: { field: 'providerId', type: 'string' },
|
|
92
|
+
sort: { field: 'sub', type: 'string' },
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
for (const provider of providers) {
|
|
96
|
+
this.#registerProvider(scope, provider, ctors);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
#registerProvider(scope, provider, ctors) {
|
|
100
|
+
const clientIdSetting = new ctors.Setting(scope, `oidc-${provider.id}-client-id`, {
|
|
101
|
+
private: true, init: () => '',
|
|
102
|
+
description: `Client ID for the ${provider.name} OIDC provider.`,
|
|
103
|
+
});
|
|
104
|
+
const clientSecretSetting = new ctors.Setting(scope, `oidc-${provider.id}-client-secret`, {
|
|
105
|
+
private: true, init: () => '',
|
|
106
|
+
description: `Client secret for the ${provider.name} OIDC provider.`,
|
|
107
|
+
});
|
|
108
|
+
const callbackEndpoint = new ctors.Endpoint(scope, `oidc-${provider.id}-callback`, {
|
|
109
|
+
description: `OIDC callback endpoint for ${provider.name}.`,
|
|
110
|
+
handle: (ctx) => this.#handleCallback(ctx, provider, clientIdSetting, clientSecretSetting),
|
|
111
|
+
});
|
|
112
|
+
const startEndpoint = new ctors.Endpoint(scope, `oidc-${provider.id}-start`, {
|
|
113
|
+
description: `Initiates OIDC sign-in with ${provider.name}.`,
|
|
114
|
+
handle: (ctx) => this.#handleStart(ctx, provider, clientIdSetting, callbackEndpoint),
|
|
115
|
+
});
|
|
116
|
+
this.#providers.set(provider.id, { provider, clientIdSetting, startEndpoint });
|
|
117
|
+
}
|
|
118
|
+
async #handleStart(context, provider, clientIdSetting, callbackEndpoint) {
|
|
119
|
+
const clientId = await clientIdSetting.read() || '';
|
|
120
|
+
if (!clientId) {
|
|
121
|
+
context.responseCode = 302;
|
|
122
|
+
context.responseHeaders['Location'] = '/';
|
|
123
|
+
return '';
|
|
124
|
+
}
|
|
125
|
+
const codeVerifier = randomBytes(32).toString('base64url');
|
|
126
|
+
const codeChallenge = createHash('sha256').update(codeVerifier).digest('base64url');
|
|
127
|
+
const state = randomBytes(16).toString('hex');
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
const redirectUri = await callbackEndpoint.determineUrl();
|
|
130
|
+
const referer = context.requestHeaders['referer'] ?? context.requestHeaders['Referer'];
|
|
131
|
+
const returnTo = (Array.isArray(referer) ? referer[0] : referer) || '/';
|
|
132
|
+
await this.#pkceTable.save({
|
|
133
|
+
state, codeVerifier, redirectUri, returnTo, providerId: provider.id,
|
|
134
|
+
createdAt: now, ttl: Math.floor(now / 1000) + PKCE_TTL_SECONDS,
|
|
135
|
+
});
|
|
136
|
+
const scopes = provider.scopes ?? ['openid', 'email', 'profile'];
|
|
137
|
+
const params = new URLSearchParams({
|
|
138
|
+
response_type: 'code', client_id: clientId, redirect_uri: redirectUri,
|
|
139
|
+
scope: scopes.join(' '), state, code_challenge: codeChallenge,
|
|
140
|
+
code_challenge_method: 'S256',
|
|
141
|
+
});
|
|
142
|
+
context.responseCode = 302;
|
|
143
|
+
context.responseHeaders['Location'] = `${provider.authorizationEndpoint}?${params}`;
|
|
144
|
+
return '';
|
|
145
|
+
}
|
|
146
|
+
async #handleCallback(context, provider, clientIdSetting, clientSecretSetting) {
|
|
147
|
+
const code = context.location.searchParams.get('code');
|
|
148
|
+
const state = context.location.searchParams.get('state');
|
|
149
|
+
const fail = () => {
|
|
150
|
+
context.responseCode = 302;
|
|
151
|
+
context.responseHeaders['Location'] = '/';
|
|
152
|
+
return '';
|
|
153
|
+
};
|
|
154
|
+
if (!code || !state)
|
|
155
|
+
return fail();
|
|
156
|
+
const pkceRecord = await this.#pkceTable.get({ state });
|
|
157
|
+
if (!pkceRecord || pkceRecord.providerId !== provider.id)
|
|
158
|
+
return fail();
|
|
159
|
+
if (pkceRecord.createdAt < Date.now() - PKCE_TTL_SECONDS * 1000)
|
|
160
|
+
return fail();
|
|
161
|
+
await this.#pkceTable.delete({ state });
|
|
162
|
+
const clientId = await clientIdSetting.read() || '';
|
|
163
|
+
const clientSecret = await clientSecretSetting.read() || '';
|
|
164
|
+
const tokens = await exchangeCode(provider, {
|
|
165
|
+
code, codeVerifier: pkceRecord.codeVerifier, clientId, clientSecret,
|
|
166
|
+
redirectUri: pkceRecord.redirectUri,
|
|
167
|
+
});
|
|
168
|
+
const identity = await extractIdentity(tokens, clientId, provider);
|
|
169
|
+
if (!identity?.sub)
|
|
170
|
+
return fail();
|
|
171
|
+
if (!identity.displayName)
|
|
172
|
+
identity.displayName = fallbackDisplayName(identity.email);
|
|
173
|
+
const user = await this.#upsertUser(provider.id, identity);
|
|
174
|
+
await this.#cookie.write(context.cookies, {
|
|
175
|
+
state: 'authenticated',
|
|
176
|
+
user: { id: `${provider.id}:${user.sub}`, username: user.email || user.sub, displayName: user.displayName, oidcProvider: provider.id },
|
|
177
|
+
});
|
|
178
|
+
context.responseCode = 302;
|
|
179
|
+
context.responseHeaders['Location'] = pkceRecord.returnTo || '/';
|
|
180
|
+
return '';
|
|
181
|
+
}
|
|
182
|
+
async #upsertUser(providerId, identity) {
|
|
183
|
+
const now = Date.now();
|
|
184
|
+
const existing = await this.#oidcUsersTable.get({ providerId, sub: identity.sub });
|
|
185
|
+
const record = {
|
|
186
|
+
providerId, sub: identity.sub, email: identity.email,
|
|
187
|
+
displayName: existing?.displayName || identity.displayName,
|
|
188
|
+
createdAt: existing?.createdAt ?? now,
|
|
189
|
+
updatedAt: now,
|
|
190
|
+
};
|
|
191
|
+
await this.#oidcUsersTable.save(record);
|
|
192
|
+
return record;
|
|
193
|
+
}
|
|
194
|
+
async getMachineActions() {
|
|
195
|
+
const actions = {};
|
|
196
|
+
for (const [, { provider, clientIdSetting, startEndpoint }] of this.#providers) {
|
|
197
|
+
const clientId = await clientIdSetting.read();
|
|
198
|
+
if (!clientId)
|
|
199
|
+
continue;
|
|
200
|
+
const href = await startEndpoint.determineUrl();
|
|
201
|
+
const key = `signin-with-${provider.id}`;
|
|
202
|
+
actions[key] = {
|
|
203
|
+
key,
|
|
204
|
+
name: `Sign in with ${provider.name}`,
|
|
205
|
+
href,
|
|
206
|
+
icon: provider.icon,
|
|
207
|
+
buttonStyle: provider.buttonStyle,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return actions;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function prebuildApi(): Promise<void>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import process from 'process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
export async function prebuildApi() {
|
|
5
|
+
const CWD = process.cwd();
|
|
6
|
+
let API_URL = '/api';
|
|
7
|
+
const indexModule = await import(path.join(CWD, 'index.js'));
|
|
8
|
+
try {
|
|
9
|
+
const backendConfigModule = await import(path.join(CWD, 'config.js'));
|
|
10
|
+
const backendConfig = backendConfigModule.default;
|
|
11
|
+
console.log("backend config found", backendConfig);
|
|
12
|
+
if (backendConfig.apiUrl) {
|
|
13
|
+
API_URL = backendConfig.apiUrl;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
console.log("No backend API config found.");
|
|
18
|
+
}
|
|
19
|
+
const apiCode = Object.keys(indexModule)
|
|
20
|
+
.map(k => `export const ${k} = apiTree(INTERNAL_API_URL, ${JSON.stringify([k])});`)
|
|
21
|
+
.join('\n');
|
|
22
|
+
const baseClient = [
|
|
23
|
+
`import { apiTree } from "wirejs-resources/hosting/client.js";`,
|
|
24
|
+
`const INTERNAL_API_URL = ${JSON.stringify(API_URL)};`,
|
|
25
|
+
].join('\n');
|
|
26
|
+
await fs.promises.writeFile(path.join(CWD, 'index.client.js'), [baseClient, apiCode].join('\n\n'));
|
|
27
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -13,6 +13,48 @@ export type User = {
|
|
|
13
13
|
* The name this user will be known by to other users.
|
|
14
14
|
*/
|
|
15
15
|
displayName: string;
|
|
16
|
+
/**
|
|
17
|
+
* When set, indicates that this user authenticated via an OIDC/OAuth2
|
|
18
|
+
* provider. The value is the provider id (e.g. `'google'`).
|
|
19
|
+
*/
|
|
20
|
+
oidcProvider?: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Configuration for an OIDC (OpenID Connect) or OAuth2 provider.
|
|
24
|
+
*/
|
|
25
|
+
export type OIDCProviderConfig = {
|
|
26
|
+
/** Unique identifier for the provider (e.g., 'google', 'facebook'). */
|
|
27
|
+
id: string;
|
|
28
|
+
/** Display name shown to users (e.g., 'Google'). */
|
|
29
|
+
name: string;
|
|
30
|
+
/** The provider's authorization endpoint URL. */
|
|
31
|
+
authorizationEndpoint: string;
|
|
32
|
+
/** The provider's token endpoint URL. */
|
|
33
|
+
tokenEndpoint: string;
|
|
34
|
+
/**
|
|
35
|
+
* The provider's JWKS URI for ID token verification.
|
|
36
|
+
* Required for OIDC-compliant providers that return an id_token.
|
|
37
|
+
*/
|
|
38
|
+
jwksUri?: string;
|
|
39
|
+
/** Expected issuer claim in ID tokens. */
|
|
40
|
+
issuer?: string;
|
|
41
|
+
/** OAuth scopes to request. Defaults to ['openid', 'email', 'profile']. */
|
|
42
|
+
scopes?: string[];
|
|
43
|
+
/**
|
|
44
|
+
* URL for fetching user info via Bearer token.
|
|
45
|
+
* Used for non-OIDC OAuth providers (e.g., GitHub) that do not return an id_token.
|
|
46
|
+
*/
|
|
47
|
+
userInfoEndpoint?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Inline SVG markup for the provider's brand icon.
|
|
50
|
+
* Displayed inside the sign-in button when provided.
|
|
51
|
+
*/
|
|
52
|
+
icon?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Inline CSS applied to the sign-in button for this provider.
|
|
55
|
+
* Allows brand-colour backgrounds, text colours, etc.
|
|
56
|
+
*/
|
|
57
|
+
buttonStyle?: string;
|
|
16
58
|
};
|
|
17
59
|
export type AuthenticationField = {
|
|
18
60
|
label: string;
|
|
@@ -36,6 +78,19 @@ export type AuthenticationState = {
|
|
|
36
78
|
};
|
|
37
79
|
export type AuthenticationMachineAction = Readonly<AuthenticationAction & {
|
|
38
80
|
key: string;
|
|
81
|
+
/**
|
|
82
|
+
* If present, the action should be rendered as a direct navigation link
|
|
83
|
+
* rather than triggering the state machine (e.g., OIDC sign-in buttons).
|
|
84
|
+
*/
|
|
85
|
+
href?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Inline SVG markup for an icon to display inside the action button/link.
|
|
88
|
+
*/
|
|
89
|
+
icon?: string;
|
|
90
|
+
/**
|
|
91
|
+
* Inline CSS applied to the action button/link element.
|
|
92
|
+
*/
|
|
93
|
+
buttonStyle?: string;
|
|
39
94
|
}>;
|
|
40
95
|
export type AuthenticationMachineState = AuthenticationState & {
|
|
41
96
|
message?: string;
|
|
@@ -71,4 +126,10 @@ export type AuthenticationServiceOptions = {
|
|
|
71
126
|
* The name of the cookie to use to store the authentication state JWT.
|
|
72
127
|
*/
|
|
73
128
|
cookie?: string;
|
|
129
|
+
/**
|
|
130
|
+
* OIDC / OAuth2 providers to enable for sign-in.
|
|
131
|
+
* For each provider, a pair of Settings (client-id and client-secret) will be created,
|
|
132
|
+
* along with start and callback Endpoints.
|
|
133
|
+
*/
|
|
134
|
+
oidcProviders?: OIDCProviderConfig[];
|
|
74
135
|
};
|
|
@@ -117,4 +117,10 @@ export type TableDefinition = {
|
|
|
117
117
|
partitionKey: KeyFieldDefinition;
|
|
118
118
|
sortKey?: KeyFieldDefinition;
|
|
119
119
|
indexes?: Index<any>[];
|
|
120
|
+
/**
|
|
121
|
+
* The name of the attribute to use as the TTL (time-to-live) field.
|
|
122
|
+
* When set, the underlying provider (e.g., DynamoDB) will automatically
|
|
123
|
+
* expire records whose TTL value (epoch seconds) is in the past.
|
|
124
|
+
*/
|
|
125
|
+
ttlAttribute?: string;
|
|
120
126
|
};
|