wirejs-resources 0.1.7-alpha → 0.1.9-alpha

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.
@@ -0,0 +1,21 @@
1
+ import { CookieJar } from "./cookie-jar.js";
2
+ type ApiMethod = (...args: any) => any;
3
+ type ApiNamespace = {
4
+ [K in string]: ApiMethod | ApiNamespace;
5
+ };
6
+ type ContextfulApiMethod<T> = T extends ((...args: infer ARGS) => infer RT) ? ((context: Context | boolean, ...args: ARGS) => RT extends Promise<any> ? RT : Promise<RT>) : never;
7
+ type ContextfulApiNamespace<T> = {
8
+ [K in keyof T]: T[K] extends ApiMethod ? ContextfulApiMethod<T[K]> : ContextfulApiNamespace<T[K]>;
9
+ };
10
+ type ContextWrapped<T extends ApiNamespace | ApiMethod> = T extends ApiMethod ? ContextfulApiMethod<T> : ContextfulApiNamespace<T>;
11
+ export declare function withContext<T extends ApiMethod | ApiNamespace>(contextWrapper: (context: Context) => T, path?: string[]): ContextWrapped<T>;
12
+ export declare function requiresContext(fnOrNS: Object): fnOrNS is (context: Context) => any;
13
+ export declare class Context {
14
+ cookies: CookieJar;
15
+ location: URL;
16
+ constructor({ cookies, location }: {
17
+ cookies: CookieJar;
18
+ location: URL;
19
+ });
20
+ }
21
+ export {};
@@ -0,0 +1,32 @@
1
+ const __requiresContext = Symbol('__requiresContext');
2
+ export function withContext(contextWrapper, path = []) {
3
+ // first param needs to be a function, which enables `Proxy` to implement `apply()`.
4
+ const fnOrNs = new Proxy(function () { }, {
5
+ apply(_target, _thisArg, args) {
6
+ const [context, ...remainingArgs] = args;
7
+ let functionOrNamespaceObject = contextWrapper(context);
8
+ console.log({ context, args, functionOrNamespaceObject, path });
9
+ for (const k of path) {
10
+ functionOrNamespaceObject = functionOrNamespaceObject[k];
11
+ }
12
+ return functionOrNamespaceObject(...remainingArgs);
13
+ },
14
+ get(_target, prop) {
15
+ if (prop === __requiresContext)
16
+ return true;
17
+ return withContext(contextWrapper, [...path, prop]);
18
+ }
19
+ });
20
+ return fnOrNs;
21
+ }
22
+ export function requiresContext(fnOrNS) {
23
+ return fnOrNS[__requiresContext] === true;
24
+ }
25
+ export class Context {
26
+ cookies;
27
+ location;
28
+ constructor({ cookies, location }) {
29
+ this.cookies = cookies;
30
+ this.location = location;
31
+ }
32
+ }
@@ -0,0 +1,30 @@
1
+ export type Cookie = {
2
+ name: string;
3
+ value: string;
4
+ httpOnly?: boolean;
5
+ secure?: boolean;
6
+ maxAge?: number;
7
+ };
8
+ export declare class CookieJar {
9
+ #private;
10
+ /**
11
+ * Initialize
12
+ */
13
+ constructor(cookie?: string);
14
+ set(cookie: Cookie): void;
15
+ get(name: string): {
16
+ name: string;
17
+ value: string;
18
+ httpOnly?: boolean;
19
+ secure?: boolean;
20
+ maxAge?: number;
21
+ } | undefined;
22
+ delete(name: string): void;
23
+ /**
24
+ * Gets a copy of all cookies.
25
+ *
26
+ * Changes made to this copy are not reflected
27
+ */
28
+ getAll(): Record<string, string>;
29
+ getSetCookies(): Cookie[];
30
+ }
@@ -0,0 +1,55 @@
1
+ export class CookieJar {
2
+ #cookies = {};
3
+ /**
4
+ * The list of cookies that have been set with `set()` which need to be
5
+ * sent to the client.
6
+ */
7
+ #setCookies = new Set();
8
+ /**
9
+ * Initialize
10
+ */
11
+ constructor(cookie) {
12
+ this.#cookies = Object.fromEntries((cookie || '')
13
+ .split(/;/g)
14
+ .map(c => {
15
+ const [k, v] = c.split('=').map(p => decodeURIComponent(p.trim()));
16
+ return [k, {
17
+ name: k,
18
+ value: v
19
+ }];
20
+ }));
21
+ }
22
+ set(cookie) {
23
+ this.#cookies[cookie.name] = { ...cookie };
24
+ this.#setCookies.add(cookie.name);
25
+ }
26
+ get(name) {
27
+ return this.#cookies[name] ? { ...this.#cookies[name] } : undefined;
28
+ }
29
+ delete(name) {
30
+ if (this.#cookies[name]) {
31
+ this.#cookies[name].value = '-- deleted --';
32
+ this.#cookies[name].maxAge = 0;
33
+ this.#setCookies.add(name);
34
+ }
35
+ }
36
+ /**
37
+ * Gets a copy of all cookies.
38
+ *
39
+ * Changes made to this copy are not reflected
40
+ */
41
+ getAll() {
42
+ const all = {};
43
+ for (const cookie of Object.values(this.#cookies)) {
44
+ all[cookie.name] = cookie.value;
45
+ }
46
+ return all;
47
+ }
48
+ getSetCookies() {
49
+ const all = [];
50
+ for (const name of this.#setCookies) {
51
+ all.push({ ...this.#cookies[name] });
52
+ }
53
+ return all;
54
+ }
55
+ }
@@ -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
+ ;
@@ -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.js ADDED
@@ -0,0 +1,6 @@
1
+ export { FileService } from './services/file.js';
2
+ export { AuthenticationService } from './services/authentication.js';
3
+ export { CookieJar } from './adapters/cookie-jar.js';
4
+ export { withContext, requiresContext, Context } from './adapters/context.js';
5
+ export { Resource } from './resource.js';
6
+ export { overrides } from './overrides.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
+ ;
@@ -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/client";`,
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
+ }
@@ -0,0 +1,11 @@
1
+ import type { FileService } from "./services/file";
2
+ import type { AuthenticationService } from "./services/authentication";
3
+ import type { Secret } from "./resources/secret";
4
+ /**
5
+ * Used by hosting providers to provide service overrides.
6
+ */
7
+ export declare const overrides: {
8
+ FileService?: typeof FileService;
9
+ AuthenticationService?: typeof AuthenticationService;
10
+ Secret?: typeof Secret;
11
+ };
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * Used by hosting providers to provide service overrides.
3
3
  */
4
- export const overrides = {};
4
+ export const overrides = {};
@@ -0,0 +1,6 @@
1
+ export declare class Resource {
2
+ scope: Resource | string;
3
+ id: string;
4
+ constructor(scope: Resource | string, id: string);
5
+ get absoluteId(): string;
6
+ }
@@ -0,0 +1,20 @@
1
+ export class Resource {
2
+ scope;
3
+ id;
4
+ constructor(scope, id) {
5
+ this.scope = scope;
6
+ this.id = id;
7
+ }
8
+ get absoluteId() {
9
+ const sanitizedId = encodeURIComponent(this.id);
10
+ if (typeof this.scope === 'string') {
11
+ return `${encodeURIComponent(this.scope)}/${sanitizedId}`;
12
+ }
13
+ else if (typeof this.scope?.id === 'string') {
14
+ return `${this.scope.absoluteId}/${sanitizedId}`;
15
+ }
16
+ else {
17
+ throw new Error("Resources must defined within a scope. Provide either a namespace string or parent resource.");
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,7 @@
1
+ import { Resource } from '../resource.js';
2
+ export declare class Secret extends Resource {
3
+ #private;
4
+ constructor(scope: Resource | string, id: string);
5
+ read(): Promise<any>;
6
+ write(data: any): Promise<void>;
7
+ }
@@ -0,0 +1,28 @@
1
+ import crypto from 'crypto';
2
+ import { Resource } from '../resource.js';
3
+ import { FileService } from '../services/file.js';
4
+ import { overrides } from '../overrides.js';
5
+ const FILENAME = 'secret';
6
+ export class Secret extends Resource {
7
+ #fileService;
8
+ #initPromise;
9
+ constructor(scope, id) {
10
+ super(scope, id);
11
+ this.#fileService = new (overrides.FileService || FileService)(this, 'files');
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;
19
+ }
20
+ async read() {
21
+ await this.#initialize();
22
+ return JSON.parse(await this.#fileService.read(FILENAME));
23
+ }
24
+ async write(data) {
25
+ await this.#initialize();
26
+ await this.#fileService.write(FILENAME, JSON.stringify(data));
27
+ }
28
+ }
@@ -0,0 +1,67 @@
1
+ import { Resource } from '../resource.js';
2
+ import type { CookieJar } from '../adapters/cookie-jar.js';
3
+ type AuthenticationInput = {
4
+ label: string;
5
+ type: 'text' | 'password';
6
+ isRequired?: boolean;
7
+ };
8
+ type Action = {
9
+ name: string;
10
+ title?: string;
11
+ description?: string;
12
+ message?: string;
13
+ inputs?: Record<string, AuthenticationInput>;
14
+ buttons?: string[];
15
+ };
16
+ type AuthenticationBaseState = {
17
+ state: 'authenticated' | 'unauthenticated';
18
+ user: string | undefined;
19
+ };
20
+ type AuthenticationState = {
21
+ state: AuthenticationBaseState;
22
+ message?: string;
23
+ actions: Record<string, Action>;
24
+ };
25
+ type PerformActionParameter = {
26
+ key: string;
27
+ inputs: Record<string, string | number | boolean>;
28
+ verb: string;
29
+ };
30
+ type AuthenticationError = {
31
+ message: string;
32
+ field?: string;
33
+ };
34
+ type AuthenticationServiceOptions = {
35
+ /**
36
+ * The number of seconds the authentication session stays alive.
37
+ */
38
+ duration?: number;
39
+ /**
40
+ * Whether to automatically extend (keep alive) an authentication session when used.
41
+ */
42
+ keepalive?: boolean;
43
+ /**
44
+ * The name of the cookie to use to store the authentication state JWT.
45
+ */
46
+ cookie?: string;
47
+ };
48
+ export declare class AuthenticationService extends Resource {
49
+ #private;
50
+ constructor(scope: Resource | string, id: string, { duration, keepalive, cookie }?: AuthenticationServiceOptions);
51
+ getSigningSecret(): Promise<Uint8Array<ArrayBufferLike>>;
52
+ get signingSecret(): Promise<Uint8Array<ArrayBufferLike>>;
53
+ getBaseState(cookies: CookieJar): Promise<AuthenticationBaseState>;
54
+ getState(cookies: CookieJar): Promise<AuthenticationState>;
55
+ setBaseState(cookies: CookieJar, user?: string): Promise<void>;
56
+ missingFieldErrors(input: Record<string, string | number | boolean>, fields: string[]): AuthenticationError[] | undefined;
57
+ setState(cookies: CookieJar, { key, inputs, verb: _verb }: PerformActionParameter): Promise<AuthenticationState | {
58
+ errors: AuthenticationError[];
59
+ }>;
60
+ buildApi(this: AuthenticationService): {
61
+ getState: (context: import("../adapters/context.js").Context | boolean) => Promise<AuthenticationState>;
62
+ setState: (context: import("../adapters/context.js").Context | boolean, options: PerformActionParameter) => Promise<AuthenticationState | {
63
+ errors: AuthenticationError[];
64
+ }>;
65
+ };
66
+ }
67
+ export {};