├── .gitkeep ├── src ├── hooks │ └── index.ts ├── types │ ├── qstash.d.ts │ ├── fastify-helmet.d.ts │ ├── @octokit │ │ └── request.d.ts │ ├── tmp.d.ts │ ├── cdn-tests.d.ts │ ├── human-readable-ids.d.ts │ ├── redis-memory-server.d.ts │ ├── why-is-node-still-running.d.ts │ ├── redis-lock.d.ts │ ├── worker-configuration.d.ts │ └── tree-sitter.d.ts ├── worker │ ├── create.ts │ ├── remote-import.ts │ ├── index.ts │ └── service-worker │ │ ├── configure │ │ └── index.ts │ │ ├── dispatchers │ │ ├── index.ts │ │ ├── activate.ts │ │ └── install.ts │ │ ├── worker-loader-register.ts │ │ ├── index.ts │ │ ├── config.ts │ │ ├── global-fetch.ts │ │ ├── main.ts │ │ ├── skip-waiting.ts │ │ ├── url.ts │ │ ├── worker-exports.ts │ │ ├── service-worker-config.ts │ │ ├── virtual.ts │ │ ├── constants.ts │ │ ├── pool-worker.ts │ │ ├── import.ts │ │ ├── worker-service-url.ts │ │ ├── transferrable.ts │ │ └── worker-loader.ts ├── noop.ts ├── data │ ├── authentication-state │ │ ├── config.ts │ │ ├── get-authentication-state.ts │ │ ├── delete-authentication-state.ts │ │ ├── index.ts │ │ ├── store.ts │ │ ├── add-authentication-state.ts │ │ └── set-authentication-state.ts │ ├── kv.ts │ ├── expiring │ │ ├── index.ts │ │ └── types.ts │ ├── seed │ │ ├── index.ts │ │ ├── happening │ │ │ ├── .gitignore │ │ │ └── index.ts │ │ ├── main.ts │ │ ├── type.ts │ │ └── seed.ts │ ├── start.ts │ ├── types.ts │ ├── authorisation │ │ ├── index.ts │ │ ├── store.ts │ │ └── types.ts │ ├── authorisation-notification │ │ ├── index.ts │ │ ├── store.ts │ │ └── types.ts │ ├── identifier │ │ ├── index.ts │ │ ├── types.ts │ │ └── schema.ts │ ├── storage │ │ ├── redis-types.ts │ │ ├── start.ts │ │ ├── index.ts │ │ ├── redis-client-helpers.ts │ │ ├── types.ts │ │ └── redis-memory.ts │ ├── user │ │ ├── schema.ts │ │ ├── index.ts │ │ ├── types.ts │ │ ├── delete-user.ts │ │ └── add-user.ts │ ├── index.ts │ ├── authentication-role │ │ ├── index.ts │ │ ├── store.ts │ │ ├── set-role.ts │ │ ├── get-role.ts │ │ └── types.ts │ ├── access-token │ │ ├── index.ts │ │ ├── get-access-token.ts │ │ ├── store.ts │ │ ├── types.ts │ │ └── create-access-token.ts │ ├── task │ │ ├── get-task.ts │ │ ├── get-task-tree.ts │ │ ├── add-task.ts │ │ ├── index.ts │ │ ├── list-tasks.ts │ │ ├── store.ts │ │ ├── create-task-happening-context.ts │ │ ├── types.ts │ │ ├── set-task.ts │ │ ├── schema.ts │ │ └── list-task-trees.ts │ ├── file │ │ ├── add-file.ts │ │ ├── is-like.ts │ │ ├── list-files.ts │ │ ├── index.ts │ │ ├── delete-file.ts │ │ ├── fetch.ts │ │ ├── get-file.ts │ │ ├── save.ts │ │ ├── unlink.ts │ │ └── set-file.ts │ ├── system-log │ │ ├── index.ts │ │ ├── store.ts │ │ ├── types.ts │ │ ├── log.ts │ │ ├── list-system-logs.ts │ │ └── schema.ts │ ├── form-meta │ │ ├── list-form-meta.ts │ │ ├── add-form-meta.ts │ │ ├── get-form-meta.ts │ │ ├── index.ts │ │ ├── types.ts │ │ ├── store.ts │ │ ├── schema.ts │ │ └── set-form-meta.ts │ ├── partner │ │ ├── get-partner.ts │ │ ├── index.ts │ │ ├── store.ts │ │ ├── list-partners.ts │ │ ├── types.ts │ │ ├── add-partner.ts │ │ └── schema.ts │ ├── attendee │ │ ├── get-attendee.ts │ │ ├── delete-attendee.ts │ │ ├── index.ts │ │ ├── store.ts │ │ ├── types.ts │ │ ├── schema.ts │ │ └── get-referenced-attendees.ts │ ├── durable-request │ │ ├── list-durable-requests.ts │ │ ├── index.ts │ │ ├── is.ts │ │ ├── store.ts │ │ ├── get-durable-request.ts │ │ ├── set-durable-request.ts │ │ ├── delete-durable-request.ts │ │ └── types.ts │ ├── happening │ │ ├── get-happening.ts │ │ ├── index.ts │ │ ├── store.ts │ │ ├── list-happenings.ts │ │ ├── set-happening.ts │ │ └── delete-happening.ts │ ├── durable-event │ │ ├── index.ts │ │ ├── list-durable-event-ids.ts │ │ ├── get-durable-event.ts │ │ ├── list-durable-events.ts │ │ ├── delete-durable-event.ts │ │ ├── store.ts │ │ ├── add-durable-event.ts │ │ └── types.ts │ ├── user-credential │ │ ├── index.ts │ │ ├── list-user-credentials.ts │ │ ├── get-user-credential.ts │ │ ├── store.ts │ │ ├── types.ts │ │ └── delete-user-credential.ts │ ├── organisation │ │ ├── index.ts │ │ ├── get-organisation.ts │ │ ├── store.ts │ │ ├── add-organisation.ts │ │ ├── list-organisations.ts │ │ ├── set-organisation.ts │ │ └── types.ts │ ├── change │ │ ├── get-change.ts │ │ ├── delete-change.ts │ │ ├── index.ts │ │ ├── add-change.ts │ │ ├── store.ts │ │ ├── set-change.ts │ │ ├── list-changes.ts │ │ ├── types.ts │ │ └── schema.ts │ ├── entries.ts │ ├── background │ │ ├── store.ts │ │ └── types.ts │ ├── cache │ │ ├── store.ts │ │ └── types.ts │ └── data.ts ├── email │ └── index.ts ├── react │ ├── server │ │ ├── paths │ │ │ ├── types.ts │ │ │ ├── config.ts │ │ │ ├── user-credential │ │ │ │ └── index.ts │ │ │ ├── invite │ │ │ │ └── index.ts │ │ │ ├── durable-event │ │ │ │ └── index.ts │ │ │ ├── home-index.tsx │ │ │ ├── logout.tsx │ │ │ ├── views.ts │ │ │ ├── branding │ │ │ │ └── index.tsx │ │ │ ├── error.tsx │ │ │ └── index.ts │ │ ├── data │ │ │ └── index.ts │ │ ├── server.css │ │ ├── server-css.ts │ │ └── index.tsx │ └── client │ │ ├── index.ts │ │ ├── components │ │ ├── checkout │ │ │ ├── index.ts │ │ │ └── checkout-empty.tsx │ │ ├── products │ │ │ └── index.ts │ │ ├── services │ │ │ └── index.ts │ │ ├── happening │ │ │ ├── create │ │ │ │ └── index.ts │ │ │ ├── index.tsx │ │ │ ├── context.ts │ │ │ ├── happening.tsx │ │ │ └── at.tsx │ │ └── icons │ │ │ ├── types.ts │ │ │ ├── svg-text.tsx │ │ │ ├── clock.tsx │ │ │ ├── lightning-bolt.tsx │ │ │ ├── x-circle.tsx │ │ │ ├── magnifying-glass.tsx │ │ │ ├── index.tsx │ │ │ ├── credit-card.tsx │ │ │ ├── user-circle.tsx │ │ │ ├── eye-dropper.tsx │ │ │ ├── bag.tsx │ │ │ ├── annotation.tsx │ │ │ ├── trash.tsx │ │ │ ├── wrench.tsx │ │ │ ├── globe-alt.tsx │ │ │ ├── scale.tsx │ │ │ ├── calendar.tsx │ │ │ ├── calendar-days.tsx │ │ │ ├── prescription-bottle.tsx │ │ │ └── globe.tsx │ │ ├── utils.ts │ │ └── pages │ │ └── paths │ │ ├── home.tsx │ │ └── index.ts ├── remote │ └── index.ts ├── authentication │ ├── index.ts │ └── authsignal.ts ├── import-references.ts ├── static-references.ts ├── view │ ├── authentication.ts │ ├── views.ts │ └── types.ts ├── sync │ ├── index.ts │ └── virtual.ts ├── tests │ ├── worker │ │ ├── index.ts │ │ └── service-worker │ │ │ ├── configured │ │ │ ├── extension.ts │ │ │ ├── store.ts │ │ │ ├── prices.ts │ │ │ ├── products.ts │ │ │ ├── config.data.ts │ │ │ ├── config.ts │ │ │ └── config.json │ │ │ ├── store │ │ │ ├── cache.ts │ │ │ ├── is.ts │ │ │ ├── delete.ts │ │ │ ├── store.ts │ │ │ ├── put.ts │ │ │ ├── head.ts │ │ │ ├── post.ts │ │ │ ├── patch.ts │ │ │ ├── get.ts │ │ │ └── register.ts │ │ │ ├── chain │ │ │ ├── a.ts │ │ │ ├── b.ts │ │ │ ├── c.ts │ │ │ └── index.ts │ │ │ ├── loop │ │ │ ├── index.ts │ │ │ ├── a.ts │ │ │ ├── b.ts │ │ │ ├── c.ts │ │ │ └── registration.ts │ │ │ ├── routes.worker.ts │ │ │ └── wait.ts │ ├── schedule │ │ └── index.ts │ ├── readme │ │ ├── worker │ │ │ ├── worker.js │ │ │ └── main.js │ │ ├── index.ts │ │ ├── cache │ │ │ └── cache.js │ │ ├── content-index │ │ │ └── index.js │ │ ├── sync │ │ │ └── sync.js │ │ └── periodic-sync │ │ │ └── periodic-sync.js │ └── cache.ts ├── storage-buckets │ ├── index.ts │ └── types.ts ├── client │ ├── index.ts │ ├── client.interface.ts │ └── interface.readonly.ts ├── content-index │ ├── index.ts │ └── dispatch.ts ├── references.ts ├── fetch │ └── index.ts ├── events │ ├── index.ts │ ├── schedule │ │ └── index.ts │ └── virtual │ │ └── index.ts ├── scheduler │ ├── index.ts │ └── container.ts ├── listen │ ├── change │ │ ├── types.ts │ │ └── index.ts │ ├── index.ts │ ├── main.ts │ ├── file │ │ └── index.ts │ ├── system-log │ │ └── index.ts │ ├── branding │ │ ├── palette.ts │ │ ├── index.ts │ │ └── logo.ts │ ├── user-credential │ │ └── index.ts │ ├── serverless.ts │ ├── task │ │ ├── index.ts │ │ ├── list-tasks.ts │ │ ├── add-task.ts │ │ └── get-task.ts │ ├── auth │ │ ├── index.ts │ │ └── anonymous.ts │ ├── body-parser.ts │ └── background.ts ├── dispatch.ts ├── virtual.ts ├── periodic-sync │ └── index.ts ├── trace.ts ├── examples │ ├── index.ts │ ├── add-partner.ts │ └── retrive-system-logs.ts ├── scheduled.ts ├── main.ts ├── static.ts ├── package.readonly.ts ├── index.ts ├── config │ └── index.ts └── start.ts ├── scripts ├── imported │ ├── config.js │ ├── package.json │ └── index.js ├── example.js ├── log-version.js ├── nop │ ├── package.json │ └── index.js ├── bridge │ └── jsx-runtime.js ├── replace-between.js └── git-info.js ├── .npmignore ├── bun.lockb ├── nodemon.json ├── .prettierignore ├── public ├── watermark.png ├── example-3.svg ├── example-2.svg ├── logo.svg ├── example-1.svg └── watermark.svg ├── .nycrc ├── tailwind.config.js ├── .gitignore ├── CONTRIBUTING.md ├── tsconfig.json ├── LICENSE └── .env.example /.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/types/qstash.d.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/worker/create.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/imported/config.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/worker/remote-import.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/noop.ts: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /src/data/authentication-state/config.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/data/kv.ts: -------------------------------------------------------------------------------- 1 | export * from "./storage/kv"; -------------------------------------------------------------------------------- /src/email/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./email"; -------------------------------------------------------------------------------- /src/react/server/paths/types.ts: -------------------------------------------------------------------------------- 1 | 2 | export {} -------------------------------------------------------------------------------- /src/remote/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./import"; -------------------------------------------------------------------------------- /src/data/expiring/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; -------------------------------------------------------------------------------- /src/data/seed/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./seed"; 2 | -------------------------------------------------------------------------------- /src/data/start.ts: -------------------------------------------------------------------------------- 1 | export * from "./storage/start"; -------------------------------------------------------------------------------- /src/react/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./pages"; -------------------------------------------------------------------------------- /src/worker/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./service-worker"; -------------------------------------------------------------------------------- /src/authentication/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./get-set"; 2 | -------------------------------------------------------------------------------- /scripts/example.js: -------------------------------------------------------------------------------- 1 | import "../esnext/example/index.js"; 2 | -------------------------------------------------------------------------------- /src/react/server/data/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./provider"; 2 | -------------------------------------------------------------------------------- /src/types/fastify-helmet.d.ts: -------------------------------------------------------------------------------- 1 | declare module "fastify-helmet"; -------------------------------------------------------------------------------- /src/import-references.ts: -------------------------------------------------------------------------------- 1 | export async function importReferences() {} -------------------------------------------------------------------------------- /src/static-references.ts: -------------------------------------------------------------------------------- 1 | export async function staticReferences() {} -------------------------------------------------------------------------------- /src/types/@octokit/request.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@octokit/request"; 2 | -------------------------------------------------------------------------------- /src/view/authentication.ts: -------------------------------------------------------------------------------- 1 | export * from "../listen/authentication"; 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | node_modules 4 | coverage 5 | .env 6 | .cache -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualstate/internal/main/bun.lockb -------------------------------------------------------------------------------- /src/react/client/components/checkout/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./checkout-empty"; -------------------------------------------------------------------------------- /src/react/client/components/products/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./products-empty"; -------------------------------------------------------------------------------- /src/react/client/components/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./services-empty"; -------------------------------------------------------------------------------- /src/react/server/paths/config.ts: -------------------------------------------------------------------------------- 1 | export interface ComponentConfig { 2 | 3 | } -------------------------------------------------------------------------------- /src/react/server/paths/user-credential/index.ts: -------------------------------------------------------------------------------- 1 | export * as list from "./list"; -------------------------------------------------------------------------------- /src/sync/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./dispatch"; 2 | export * from "./manager"; -------------------------------------------------------------------------------- /src/tests/worker/index.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | await import("./service-worker"); -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["esnext/.builtAt", ".env", "package.json"] 3 | } 4 | -------------------------------------------------------------------------------- /src/data/types.ts: -------------------------------------------------------------------------------- 1 | export type * from "./data"; 2 | export * from "./storage/types"; -------------------------------------------------------------------------------- /src/storage-buckets/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./manager"; 2 | export * from "./types"; -------------------------------------------------------------------------------- /src/types/tmp.d.ts: -------------------------------------------------------------------------------- 1 | declare module "tmp" { 2 | export type DirResult = any; 3 | } -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./client"; 2 | export * from "./interface.readonly"; -------------------------------------------------------------------------------- /src/data/authorisation/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./store"; -------------------------------------------------------------------------------- /src/tests/worker/service-worker/configured/extension.ts: -------------------------------------------------------------------------------- 1 | console.log("Inside extension"); -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | node_modules 4 | esnext 5 | coverage 6 | *.md 7 | .env -------------------------------------------------------------------------------- /src/content-index/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./content-index"; 2 | export * from "./dispatch"; -------------------------------------------------------------------------------- /public/watermark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualstate/internal/main/public/watermark.png -------------------------------------------------------------------------------- /src/data/authorisation-notification/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./store"; -------------------------------------------------------------------------------- /src/data/expiring/types.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Expiring { 3 | expiresAt?: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/react/server/server.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/references.ts: -------------------------------------------------------------------------------- 1 | export * from "./static-references"; 2 | export * from "./import-references"; -------------------------------------------------------------------------------- /src/types/cdn-tests.d.ts: -------------------------------------------------------------------------------- 1 | declare module "https://cdn.skypack.dev/@virtualstate/listen/tests"; 2 | -------------------------------------------------------------------------------- /src/worker/service-worker/configure/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./import"; 2 | export * from "./types"; -------------------------------------------------------------------------------- /src/data/identifier/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * as identifierSchema from "./schema" -------------------------------------------------------------------------------- /src/react/client/components/happening/create/index.ts: -------------------------------------------------------------------------------- 1 | export function CreateHappening() { 2 | 3 | } -------------------------------------------------------------------------------- /src/data/storage/redis-types.ts: -------------------------------------------------------------------------------- 1 | import type {Redis} from "ioredis"; 2 | 3 | export type RedisClient = Redis -------------------------------------------------------------------------------- /src/fetch/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./events"; 2 | export * from "./cache"; 3 | export * from "./dispatch"; -------------------------------------------------------------------------------- /src/events/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./dispatch"; 2 | export * from "./schedule"; 3 | export * from "./virtual"; -------------------------------------------------------------------------------- /src/react/server/paths/invite/index.ts: -------------------------------------------------------------------------------- 1 | export * as create from "./create"; 2 | export * as accept from "./accept"; -------------------------------------------------------------------------------- /src/scheduler/index.ts: -------------------------------------------------------------------------------- 1 | import { Scheduler } from "./container"; 2 | 3 | export const scheduler = new Scheduler(); -------------------------------------------------------------------------------- /src/react/server/paths/durable-event/index.ts: -------------------------------------------------------------------------------- 1 | export * as create from "./schedule"; 2 | export * as list from "./list"; -------------------------------------------------------------------------------- /src/tests/schedule/index.ts: -------------------------------------------------------------------------------- 1 | await import("./dispatch"); 2 | await import("./immediate"); 3 | 4 | export default 1; -------------------------------------------------------------------------------- /src/data/user/schema.ts: -------------------------------------------------------------------------------- 1 | // TODO not yet used through the API, is for the UI instead 2 | 3 | export const userData = 1; 4 | -------------------------------------------------------------------------------- /src/data/seed/happening/.gitignore: -------------------------------------------------------------------------------- 1 | maintained.csv 2 | maintained-cc.csv 3 | maintained-cc.js 4 | reddit-*.csv 5 | reddit-*.json -------------------------------------------------------------------------------- /src/listen/change/types.ts: -------------------------------------------------------------------------------- 1 | export interface ChangeTargetParams { 2 | targetType: string; 3 | changeType: string; 4 | } -------------------------------------------------------------------------------- /src/react/server/paths/home-index.tsx: -------------------------------------------------------------------------------- 1 | export { 2 | Component, 3 | anonymous 4 | } from "./home"; 5 | 6 | export const path = "/"; -------------------------------------------------------------------------------- /src/data/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./data"; 2 | export * from "./seed"; 3 | export * from "./storage"; 4 | export * from "./expiring-kv"; -------------------------------------------------------------------------------- /src/react/client/components/happening/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./context"; 2 | export * from "./happening"; 3 | export * from "./create"; -------------------------------------------------------------------------------- /src/worker/service-worker/dispatchers/index.ts: -------------------------------------------------------------------------------- 1 | import "../../../dispatch"; 2 | export * from "./activate"; 3 | export * from "./install"; -------------------------------------------------------------------------------- /src/data/identifier/types.ts: -------------------------------------------------------------------------------- 1 | export interface Identifier { 2 | type: string; 3 | identifier: string; 4 | identifiedAt: string; 5 | } -------------------------------------------------------------------------------- /src/dispatch.ts: -------------------------------------------------------------------------------- 1 | import "./events/dispatch"; 2 | import "./fetch/dispatch"; 3 | import "./periodic-sync/dispatch"; 4 | import "./sync/dispatch"; -------------------------------------------------------------------------------- /src/types/human-readable-ids.d.ts: -------------------------------------------------------------------------------- 1 | declare module "human-readable-ids" { 2 | export const hri: { 3 | random(): string 4 | } 5 | } -------------------------------------------------------------------------------- /src/virtual.ts: -------------------------------------------------------------------------------- 1 | export * from "./sync/virtual"; 2 | export * from "./periodic-sync/virtual"; 3 | export * from "./worker/service-worker/virtual"; -------------------------------------------------------------------------------- /src/listen/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./listen"; 2 | export * from "./routes"; 3 | export * from "./authentication"; 4 | export * from "./config"; 5 | -------------------------------------------------------------------------------- /src/periodic-sync/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./dispatch"; 2 | export * from "./manager"; 3 | export * from "./schedule"; 4 | export * from "./virtual"; -------------------------------------------------------------------------------- /src/tests/worker/service-worker/store/cache.ts: -------------------------------------------------------------------------------- 1 | import {caches} from "../../../../fetch"; 2 | 3 | export const cache = await caches.open("store"); -------------------------------------------------------------------------------- /src/worker/service-worker/worker-loader-register.ts: -------------------------------------------------------------------------------- 1 | import { register } from "node:module"; 2 | 3 | register("./worker-loader.js", import.meta.url); -------------------------------------------------------------------------------- /src/react/client/components/icons/types.ts: -------------------------------------------------------------------------------- 1 | import { HTMLProps } from "react"; 2 | 3 | export interface IconProps extends HTMLProps {} 4 | -------------------------------------------------------------------------------- /src/data/authentication-role/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./store"; 3 | export * from "./get-role"; 4 | export * from "./set-role"; -------------------------------------------------------------------------------- /src/trace.ts: -------------------------------------------------------------------------------- 1 | import opentelemetry from "@opentelemetry/api"; 2 | 3 | export const tracer = opentelemetry.trace.getTracer("@opennetwork/happening"); 4 | 5 | -------------------------------------------------------------------------------- /src/data/seed/happening/index.ts: -------------------------------------------------------------------------------- 1 | import { seed as staticSeed } from "./static-initial"; 2 | 3 | export async function seed() { 4 | await staticSeed(); 5 | } 6 | -------------------------------------------------------------------------------- /src/worker/service-worker/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./container"; 2 | export * from "./execute-fetch"; 3 | export * from "./execute"; 4 | export * from "./configure"; -------------------------------------------------------------------------------- /src/worker/service-worker/config.ts: -------------------------------------------------------------------------------- 1 | import {SERVICE_WORKER_URL} from "../../config"; 2 | 3 | export function isServiceWorker() { 4 | return SERVICE_WORKER_URL 5 | } -------------------------------------------------------------------------------- /src/examples/index.ts: -------------------------------------------------------------------------------- 1 | export * as addPartner from "./add-partner"; 2 | export * as retrieveSystemLogs from "./retrive-system-logs"; 3 | 4 | // TODO: Export examples here -------------------------------------------------------------------------------- /src/data/access-token/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./store"; 2 | export * from "./types"; 3 | export * from "./get-access-token"; 4 | export * from "./create-access-token"; 5 | 6 | -------------------------------------------------------------------------------- /scripts/log-version.js: -------------------------------------------------------------------------------- 1 | import("fs") 2 | .then(({ promises }) => promises.readFile("package.json")) 3 | .then(JSON.parse) 4 | .then(({ version }) => console.log(version)); 5 | -------------------------------------------------------------------------------- /src/data/task/get-task.ts: -------------------------------------------------------------------------------- 1 | import { getTaskStore } from "./store"; 2 | 3 | export function getTask(id: string) { 4 | const store = getTaskStore(); 5 | return store.get(id); 6 | } 7 | -------------------------------------------------------------------------------- /src/data/file/add-file.ts: -------------------------------------------------------------------------------- 1 | import { FileData } from "./types"; 2 | import { setFile } from "./set-file"; 3 | 4 | export function addFile(data: FileData) { 5 | return setFile(data); 6 | } 7 | -------------------------------------------------------------------------------- /src/events/schedule/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./event"; 2 | export { addEventListener, removeEventListener, on, cron, ScheduledFunctionOptions, ScheduledOptions, ScheduledFn } from "./schedule"; -------------------------------------------------------------------------------- /src/tests/readme/worker/worker.js: -------------------------------------------------------------------------------- 1 | self.addEventListener("fetch", event => { 2 | console.log(event.request.method, event.request.url); 3 | event.respondWith(new Response("Hello")) 4 | }); -------------------------------------------------------------------------------- /src/data/system-log/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./store"; 3 | export * from "./list-system-logs"; 4 | export * from "./log"; 5 | export * as systemLogSchema from "./schema"; -------------------------------------------------------------------------------- /src/scheduled.ts: -------------------------------------------------------------------------------- 1 | import "./email"; 2 | import "./events/virtual" 3 | import "./events/dispatch"; 4 | import "./fetch"; 5 | import "./sync"; 6 | import "./content-index"; 7 | import "./periodic-sync"; -------------------------------------------------------------------------------- /src/data/form-meta/list-form-meta.ts: -------------------------------------------------------------------------------- 1 | import { getFormMetaStore } from "./store"; 2 | 3 | export function listFormMeta() { 4 | const store = getFormMetaStore(); 5 | return store.values(); 6 | } 7 | -------------------------------------------------------------------------------- /scripts/nop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "main": "./index.js", 4 | "type": "module", 5 | "exports": { 6 | ".": "./index.js", 7 | "./": "./index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /scripts/imported/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "main": "./index.js", 4 | "type": "module", 5 | "exports": { 6 | ".": "./index.js", 7 | "./": "./index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/data/seed/main.ts: -------------------------------------------------------------------------------- 1 | // This is the main file for running the seed directly as a script 2 | import { config } from "dotenv"; 3 | 4 | config(); 5 | 6 | import { seed } from "./seed"; 7 | 8 | await seed({}); 9 | -------------------------------------------------------------------------------- /src/worker/service-worker/global-fetch.ts: -------------------------------------------------------------------------------- 1 | declare var _ORIGINAL_GLOBAL_FETCH: typeof fetch; 2 | 3 | export const globalFetch = typeof _ORIGINAL_GLOBAL_FETCH === "undefined" ? fetch : _ORIGINAL_GLOBAL_FETCH; 4 | -------------------------------------------------------------------------------- /src/data/partner/get-partner.ts: -------------------------------------------------------------------------------- 1 | import { getPartnerStore } from "./store"; 2 | 3 | export function getPartner(partnerId: string) { 4 | const store = getPartnerStore(); 5 | return store.get(partnerId); 6 | } 7 | -------------------------------------------------------------------------------- /src/data/storage/start.ts: -------------------------------------------------------------------------------- 1 | import {stopRedis} from "./redis-client"; 2 | import {stopKVConnect} from "./kv-connect"; 3 | 4 | export async function stopData() { 5 | await stopRedis(); 6 | stopKVConnect(); 7 | } -------------------------------------------------------------------------------- /src/data/attendee/get-attendee.ts: -------------------------------------------------------------------------------- 1 | import {getAttendeeStore} from "./store"; 2 | 3 | export function getAttendee(attendeeId: string) { 4 | const store = getAttendeeStore(); 5 | return store.get(attendeeId); 6 | } -------------------------------------------------------------------------------- /src/data/form-meta/add-form-meta.ts: -------------------------------------------------------------------------------- 1 | import { FormMetaData } from "./types"; 2 | import { setFormMeta } from "./set-form-meta"; 3 | 4 | export function addFormMeta(data: FormMetaData) { 5 | return setFormMeta(data); 6 | } 7 | -------------------------------------------------------------------------------- /src/data/form-meta/get-form-meta.ts: -------------------------------------------------------------------------------- 1 | import { getFormMetaStore } from "./store"; 2 | 3 | export function getFormMeta(formMetaId: string) { 4 | const store = getFormMetaStore(); 5 | return store.get(formMetaId); 6 | } 7 | -------------------------------------------------------------------------------- /src/data/attendee/delete-attendee.ts: -------------------------------------------------------------------------------- 1 | import {getAttendeeStore} from "./store"; 2 | 3 | export async function deleteAttendee(attendeeId: string) { 4 | const store = getAttendeeStore(); 5 | return store.delete(attendeeId); 6 | } -------------------------------------------------------------------------------- /src/data/durable-request/list-durable-requests.ts: -------------------------------------------------------------------------------- 1 | import {getDurableRequestStore} from "./store"; 2 | 3 | export function listDurableRequests() { 4 | const store = getDurableRequestStore(); 5 | return store.values(); 6 | } -------------------------------------------------------------------------------- /src/data/happening/get-happening.ts: -------------------------------------------------------------------------------- 1 | import {getHappeningStore} from "./store"; 2 | 3 | export async function getHappening(happeningId: string) { 4 | const store = getHappeningStore(); 5 | return store.get(happeningId); 6 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "./references"; 3 | 4 | import * as dotenv from "dotenv"; 5 | 6 | dotenv.config(); 7 | 8 | const { start } = await import("./start"); 9 | 10 | export const stop = await start(); -------------------------------------------------------------------------------- /src/data/attendee/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./get-attendee"; 3 | export * from "./set-attendee"; 4 | export * from "./store"; 5 | export * from "./delete-attendee"; 6 | export * as attendeeSchema from "./schema"; -------------------------------------------------------------------------------- /src/data/partner/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./store"; 2 | export * from "./types"; 3 | export * from "./get-partner"; 4 | export * from "./list-partners"; 5 | export * from "./add-partner"; 6 | export * as partnerSchema from "./schema"; 7 | -------------------------------------------------------------------------------- /src/worker/service-worker/main.ts: -------------------------------------------------------------------------------- 1 | import {isServiceWorker} from "./config"; 2 | 3 | export let close = async () => {}; 4 | 5 | if (isServiceWorker()) { 6 | const { start } = await import("./start"); 7 | close = await start(); 8 | } -------------------------------------------------------------------------------- /src/data/access-token/get-access-token.ts: -------------------------------------------------------------------------------- 1 | import {getAccessTokenStore} from "./store"; 2 | 3 | export async function getAccessToken(accessToken: string) { 4 | const store = getAccessTokenStore(); 5 | return store.get(accessToken); 6 | } -------------------------------------------------------------------------------- /src/data/seed/type.ts: -------------------------------------------------------------------------------- 1 | export interface SeedOptions extends Record { 2 | seed?: string; 3 | } 4 | 5 | export interface Seed { 6 | seed(options: SeedOptions): void | Promise; 7 | seed(): void | Promise; 8 | } 9 | -------------------------------------------------------------------------------- /src/tests/worker/service-worker/store/is.ts: -------------------------------------------------------------------------------- 1 | export function isMaybeJSONContentType({ headers }: { headers: Headers }) { 2 | if (!headers.has("Content-Type")) return true; 3 | return headers.get("Content-Type") === "application/json"; 4 | } -------------------------------------------------------------------------------- /src/data/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./add-user"; 2 | export * from "./get-user"; 3 | export * as userSchema from "./schema"; 4 | export * from "./set-user"; 5 | export * from "./store"; 6 | export * from "./types"; 7 | export * from "./delete-user"; -------------------------------------------------------------------------------- /src/tests/readme/index.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | 3 | await import("./worker/main.js"); 4 | await import("./cache/cache.js"); 5 | await import("./content-index/index.js"); 6 | await import("./sync/sync.js"); 7 | await import("./periodic-sync/periodic-sync.js"); -------------------------------------------------------------------------------- /src/types/redis-memory-server.d.ts: -------------------------------------------------------------------------------- 1 | declare module "redis-memory-server" { 2 | 3 | export class RedisMemoryServer { 4 | getHost(): Promise; 5 | getPort(): Promise; 6 | stop(): Promise; 7 | } 8 | } -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | ], 4 | "reporter": [ 5 | "clover", 6 | "json-summary", 7 | "html", 8 | "text-summary" 9 | ], 10 | "branches": 80, 11 | "lines": 80, 12 | "functions": 80, 13 | "statements": 80 14 | } 15 | -------------------------------------------------------------------------------- /src/data/durable-event/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./store"; 2 | export * from "./types"; 3 | export * from "./delete-durable-event"; 4 | export * from "./add-durable-event"; 5 | export * from "./get-durable-event"; 6 | export * from "./list-durable-events"; 7 | -------------------------------------------------------------------------------- /src/data/user-credential/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./store"; 2 | export * from "./types"; 3 | export * from "./list-user-credentials"; 4 | export * from "./set-user-credential"; 5 | export * from "./delete-user-credential"; 6 | export * from "./get-user-credential"; -------------------------------------------------------------------------------- /src/worker/service-worker/skip-waiting.ts: -------------------------------------------------------------------------------- 1 | import {DurableServiceWorkerScope} from "./types"; 2 | import {getServiceWorkerId} from "./service-worker-config"; 3 | 4 | export async function skipWaiting() { 5 | const serviceWorkerId = getServiceWorkerId(); 6 | } -------------------------------------------------------------------------------- /src/tests/worker/service-worker/chain/a.ts: -------------------------------------------------------------------------------- 1 | import {requestMethod} from "../routes.example"; 2 | 3 | requestMethod.post("/", async (request) => { 4 | const text = await request.text(); 5 | return new Response(`${text}\nAdded line from A: ${Math.random()}`) 6 | }); -------------------------------------------------------------------------------- /src/worker/service-worker/url.ts: -------------------------------------------------------------------------------- 1 | export function getURLSource(input: URL | RequestInfo) { 2 | if (input instanceof URL) { 3 | return input; 4 | } 5 | if (typeof input === "string") { 6 | return input; 7 | } 8 | return input.url; 9 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import forms from "@tailwindcss/forms"; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | export default { 5 | content: ["./src/**/*.{html,js,ts,tsx,jsx}"], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [forms], 10 | }; 11 | -------------------------------------------------------------------------------- /src/data/authentication-state/get-authentication-state.ts: -------------------------------------------------------------------------------- 1 | import { getAuthenticationStateStore } from "./store"; 2 | 3 | export async function getAuthenticationState(stateId: string) { 4 | const store = getAuthenticationStateStore(); 5 | return store.get(stateId); 6 | } 7 | -------------------------------------------------------------------------------- /src/data/form-meta/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./add-form-meta"; 2 | export * from "./get-form-meta"; 3 | export * from "./list-form-meta"; 4 | export * from "./set-form-meta"; 5 | export * from "./store"; 6 | export * from "./types"; 7 | export * as formMetaSchema from "./schema"; 8 | -------------------------------------------------------------------------------- /src/data/authentication-state/delete-authentication-state.ts: -------------------------------------------------------------------------------- 1 | import { getAuthenticationStateStore } from "./store"; 2 | 3 | export async function deleteAuthenticationState(stateId: string) { 4 | const store = getAuthenticationStateStore(); 5 | await store.delete(stateId); 6 | } 7 | -------------------------------------------------------------------------------- /src/data/durable-request/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./store"; 3 | export * from "./set-durable-request"; 4 | export * from "./delete-durable-request"; 5 | export * from "./get-durable-request"; 6 | export * from "./list-durable-requests"; 7 | export * from "./from"; -------------------------------------------------------------------------------- /src/tests/worker/service-worker/chain/b.ts: -------------------------------------------------------------------------------- 1 | import {requestMethod} from "../routes.example"; 2 | import {v4} from "uuid"; 3 | 4 | requestMethod.post("/", async (request) => { 5 | const text = await request.text(); 6 | return new Response(`${text}\nAdded line from B: ${v4()}`) 7 | }); -------------------------------------------------------------------------------- /scripts/bridge/jsx-runtime.js: -------------------------------------------------------------------------------- 1 | import * as JSXRuntime from "react/jsx-runtime"; 2 | 3 | export const Fragment = JSXRuntime.Fragment ?? JSXRuntime.default.Fragment; 4 | export const jsx = JSXRuntime.jsx ?? JSXRuntime.default.jsx; 5 | export const jsxs = JSXRuntime.jsxs ?? JSXRuntime.default.jsxs; -------------------------------------------------------------------------------- /src/data/form-meta/types.ts: -------------------------------------------------------------------------------- 1 | export interface FormMetaData extends Record {} 2 | 3 | export interface FormMeta extends FormMetaData { 4 | formMetaId: string; 5 | userId?: string; 6 | partnerId?: string; 7 | createdAt: string; 8 | updatedAt: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/data/organisation/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./store"; 2 | export * from "./types"; 3 | export * from "./get-organisation"; 4 | export * from "./list-organisations"; 5 | export * from "./add-organisation"; 6 | export * from "./set-organisation"; 7 | export * as organisationSchema from "./schema"; -------------------------------------------------------------------------------- /src/react/server/server-css.ts: -------------------------------------------------------------------------------- 1 | import { join, dirname } from "node:path"; 2 | import { readFile } from "node:fs/promises"; 3 | 4 | const { pathname } = new URL(import.meta.url); 5 | 6 | const path = join(dirname(pathname), "server.css"); 7 | 8 | export default await readFile(path, "utf-8"); 9 | -------------------------------------------------------------------------------- /src/types/why-is-node-still-running.d.ts: -------------------------------------------------------------------------------- 1 | declare module "why-is-node-still-running" { 2 | export function whyIsNodeStillRunning(): void; 3 | export interface WhyModule { 4 | whyIsNodeStillRunning: () => void, 5 | } 6 | const why: WhyModule; 7 | export default why; 8 | } -------------------------------------------------------------------------------- /src/data/task/get-task-tree.ts: -------------------------------------------------------------------------------- 1 | import {getHappeningTree} from "../happening"; 2 | import {createTaskHappeningTreeContext} from "./create-task-happening-context"; 3 | 4 | export async function getTaskTree(taskId: string) { 5 | return getHappeningTree(taskId, createTaskHappeningTreeContext()); 6 | } -------------------------------------------------------------------------------- /src/tests/worker/service-worker/configured/store.ts: -------------------------------------------------------------------------------- 1 | import {FetchStore} from "../fetch-store"; 2 | 3 | export const json = new FetchStore({ 4 | type: "json", 5 | headers: { 6 | "Content-Type": "application/json" 7 | }, 8 | body: JSON.stringify, 9 | fetch 10 | }); -------------------------------------------------------------------------------- /src/data/attendee/store.ts: -------------------------------------------------------------------------------- 1 | import {getKeyValueStore} from "../kv"; 2 | import {Attendee} from "./types"; 3 | 4 | const STORE_NAME = "attendee" as const; 5 | 6 | export function getAttendeeStore() { 7 | return getKeyValueStore(STORE_NAME, { 8 | counter: true 9 | }); 10 | } -------------------------------------------------------------------------------- /src/data/durable-event/list-durable-event-ids.ts: -------------------------------------------------------------------------------- 1 | import {DurableEventTypeData} from "./types"; 2 | import {getDurableEventStore} from "./store"; 3 | 4 | export function listDurableEventIds(event: DurableEventTypeData) { 5 | const store = getDurableEventStore(event); 6 | return store.keys(); 7 | } -------------------------------------------------------------------------------- /src/data/form-meta/store.ts: -------------------------------------------------------------------------------- 1 | import { getKeyValueStore } from "../kv"; 2 | import { FormMeta } from "./types"; 3 | 4 | const STORE_NAME = "formMeta"; 5 | 6 | export function getFormMetaStore() { 7 | return getKeyValueStore(STORE_NAME, { 8 | counter: false, 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /src/data/partner/store.ts: -------------------------------------------------------------------------------- 1 | import { getKeyValueStore } from "../kv"; 2 | import { Partner } from "./types"; 3 | 4 | const STORE_NAME = "partner" as const; 5 | 6 | export function getPartnerStore() { 7 | return getKeyValueStore(STORE_NAME, { 8 | counter: true 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /src/data/storage/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./kv"; 3 | export * from "./redis-memory"; 4 | export * from "./redis-client"; 5 | export * from "./redis-client-helpers"; 6 | export * from "./lock"; 7 | export * from "./start"; 8 | export * from "./hook"; 9 | export * from "./memory"; -------------------------------------------------------------------------------- /src/data/change/get-change.ts: -------------------------------------------------------------------------------- 1 | import { getChangeStore } from "./store"; 2 | import {Change, ChangeIdentifier} from "./types"; 3 | 4 | export async function getChange(identifier: ChangeIdentifier): Promise { 5 | const store = getChangeStore(identifier); 6 | return store.get(identifier.changeId); 7 | } -------------------------------------------------------------------------------- /src/data/entries.ts: -------------------------------------------------------------------------------- 1 | import {ok} from "../is"; 2 | 3 | export type Entries = { 4 | [K in keyof T]: [K, T[K]]; 5 | }[keyof T][]; 6 | 7 | export function entries(value: T): Entries { 8 | const result = Object.entries(value); 9 | ok>(result); 10 | return result; 11 | } -------------------------------------------------------------------------------- /src/tests/worker/service-worker/chain/c.ts: -------------------------------------------------------------------------------- 1 | import {requestMethod} from "../routes.example"; 2 | import {v4} from "uuid"; 3 | 4 | requestMethod.post("/", async (request) => { 5 | const text = await request.text(); 6 | return new Response(`${text}\nAdded line from C: ${v4()} + ${Math.random()}`) 7 | }); -------------------------------------------------------------------------------- /src/data/access-token/store.ts: -------------------------------------------------------------------------------- 1 | import {getKeyValueStore} from "../kv"; 2 | import {AccessToken} from "./types"; 3 | 4 | const STORE_NAME = "accessToken" 5 | 6 | export function getAccessTokenStore() { 7 | return getKeyValueStore(STORE_NAME, { 8 | counter: false 9 | }); 10 | } -------------------------------------------------------------------------------- /src/data/change/delete-change.ts: -------------------------------------------------------------------------------- 1 | import { getChangeStore } from "./store"; 2 | import { ChangeIdentifier } from "./types"; 3 | 4 | export async function deleteChange(identifier: ChangeIdentifier): Promise { 5 | const store = getChangeStore(identifier); 6 | return store.delete(identifier.changeId); 7 | } -------------------------------------------------------------------------------- /src/data/system-log/store.ts: -------------------------------------------------------------------------------- 1 | import {getKeyValueStore} from "../kv"; 2 | import {SystemLog} from "./types"; 3 | 4 | const STORE_NAME = "systemLog" as const; 5 | 6 | export function getSystemLogStore() { 7 | return getKeyValueStore(STORE_NAME, { 8 | counter: false 9 | }); 10 | } -------------------------------------------------------------------------------- /src/listen/main.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "../references"; 3 | 4 | import * as dotenv from "dotenv"; 5 | 6 | dotenv.config(); 7 | 8 | import "../tracing"; 9 | 10 | await import("../scheduled"); 11 | 12 | const { listen } = await import("./listen"); 13 | 14 | export const close = await listen(); -------------------------------------------------------------------------------- /src/data/background/store.ts: -------------------------------------------------------------------------------- 1 | import {getKeyValueStore} from "../kv"; 2 | import {Background} from "./types"; 3 | 4 | const STORE_NAME = "background" as const; 5 | 6 | export function getBackgroundStore() { 7 | return getKeyValueStore(STORE_NAME, { 8 | counter: false 9 | }); 10 | } -------------------------------------------------------------------------------- /src/data/change/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./store"; 2 | export * from "./types"; 3 | export * from "./get-change"; 4 | export * from "./list-changes"; 5 | export * from "./add-change"; 6 | export * from "./set-change"; 7 | export * from "./delete-change"; 8 | export * as changeSchema from "./schema"; 9 | export * from "./change"; -------------------------------------------------------------------------------- /src/data/task/add-task.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from "uuid"; 2 | import { TaskData, Task } from "./types"; 3 | import { setTask } from "./set-task"; 4 | 5 | export async function addTask(data: TaskData): Promise { 6 | const taskId = v4(); 7 | return setTask({ 8 | ...data, 9 | taskId, 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /src/data/task/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./store"; 2 | export * from "./types"; 3 | export * from "./get-task"; 4 | export * from "./list-tasks"; 5 | export * from "./add-task"; 6 | export * from "./set-task"; 7 | export * as taskSchema from "./schema"; 8 | export * from "./list-task-trees"; 9 | export * from "./get-task-tree"; -------------------------------------------------------------------------------- /src/tests/worker/service-worker/store/delete.ts: -------------------------------------------------------------------------------- 1 | import {requestMethod} from "../routes.example"; 2 | import {cache} from "./cache"; 3 | 4 | requestMethod.delete({ pathname: "/:type/:id" }, async request => { 5 | await cache.delete(request.url); 6 | return new Response(null, { 7 | status: 204 8 | }); 9 | }) -------------------------------------------------------------------------------- /src/data/file/is-like.ts: -------------------------------------------------------------------------------- 1 | import {FileData, File} from "./types"; 2 | import {isLike} from "../../is"; 3 | 4 | export function isFileLike(file: unknown): file is FileData & Partial { 5 | return !!( 6 | isLike(file) && 7 | file.fileName && 8 | file.contentType 9 | ); 10 | } -------------------------------------------------------------------------------- /src/data/task/list-tasks.ts: -------------------------------------------------------------------------------- 1 | import { Task } from "./types"; 2 | import { getTaskStore } from "./store"; 3 | 4 | export interface ListTasksInput {} 5 | 6 | export async function listTasks({}: ListTasksInput = {}): Promise< 7 | Task[] 8 | > { 9 | const store = getTaskStore(); 10 | return store.values(); 11 | } 12 | -------------------------------------------------------------------------------- /src/data/authorisation/store.ts: -------------------------------------------------------------------------------- 1 | import {getKeyValueStore} from "../kv"; 2 | import {Authorisation} from "./types"; 3 | 4 | const STORE_NAME = "authorisation" as const; 5 | 6 | export function getAuthorisationStore() { 7 | return getKeyValueStore(STORE_NAME, { 8 | counter: false 9 | }); 10 | } -------------------------------------------------------------------------------- /src/data/organisation/get-organisation.ts: -------------------------------------------------------------------------------- 1 | import { getOrganisationStore } from "./store"; 2 | import {Organisation} from "./types"; 3 | 4 | export function getOrganisation(organisationId: string) { 5 | const store = getOrganisationStore(); 6 | return store.get(organisationId); 7 | } 8 | -------------------------------------------------------------------------------- /src/data/user-credential/list-user-credentials.ts: -------------------------------------------------------------------------------- 1 | import {UserCredential} from "./types"; 2 | import {getUserCredentialStore} from "./store"; 3 | 4 | export async function listUserCredentials(userId: string): Promise< 5 | UserCredential[] 6 | > { 7 | const store = getUserCredentialStore(userId); 8 | return store.values(); 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | node_modules 4 | esnext 5 | esnext-workerd 6 | coverage 7 | .env 8 | .env.test 9 | /test-results/ 10 | /playwright-report/ 11 | /playwright/.cache/ 12 | workerd-tests 13 | workerd-tests.capnp 14 | .cache 15 | .vercel 16 | kv.db 17 | kv.db-shm 18 | kv.db-wal 19 | db.db 20 | db.db-shm 21 | db.db-wal 22 | -------------------------------------------------------------------------------- /src/data/authentication-state/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./store"; 3 | export * from "./add-authentication-state"; 4 | export * from "./get-authentication-state"; 5 | export * from "./set-authentication-state"; 6 | export * from "./roles"; 7 | export * from "./delete-authentication-state"; 8 | export * from "./invite"; -------------------------------------------------------------------------------- /src/data/task/store.ts: -------------------------------------------------------------------------------- 1 | import { getKeyValueStore } from "../kv"; 2 | import { Task } from "./types"; 3 | import {getHappeningStore} from "../happening"; 4 | 5 | const STORE_NAME = "task" as const; 6 | 7 | export function getTaskStore() { 8 | return getHappeningStore(STORE_NAME, { 9 | counter: true 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /src/data/change/add-change.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from "uuid"; 2 | import { ChangeData, Change } from "./types"; 3 | import { setChange } from "./set-change"; 4 | 5 | export async function addChange(data: ChangeData): Promise { 6 | const changeId = v4(); 7 | return setChange({ 8 | ...data, 9 | changeId, 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /src/data/organisation/store.ts: -------------------------------------------------------------------------------- 1 | import { getKeyValueStore } from "../kv"; 2 | import { Organisation } from "./types"; 3 | 4 | const STORE_NAME = "organisation" as const; 5 | 6 | export function getOrganisationStore() { 7 | return getKeyValueStore(STORE_NAME, { 8 | counter: true 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /src/worker/service-worker/worker-exports.ts: -------------------------------------------------------------------------------- 1 | declare var _GLOBAL_getServiceWorkerModuleExports: () => Record; 2 | 3 | export function getServiceWorkerModuleExports() { 4 | if (typeof _GLOBAL_getServiceWorkerModuleExports === "undefined") { 5 | return {}; 6 | } 7 | return _GLOBAL_getServiceWorkerModuleExports(); 8 | } -------------------------------------------------------------------------------- /scripts/nop/index.js: -------------------------------------------------------------------------------- 1 | export const listenAndServe = undefined; 2 | export const createServer = undefined; 3 | 4 | /** 5 | * @type {any} 6 | */ 7 | const on = () => {}; 8 | /** 9 | * @type {any} 10 | */ 11 | const env = {}; 12 | 13 | /*** 14 | * @type {any} 15 | */ 16 | export default { 17 | env, 18 | on, 19 | exit(arg) {}, 20 | }; 21 | -------------------------------------------------------------------------------- /src/data/authentication-role/store.ts: -------------------------------------------------------------------------------- 1 | import {getExpiringStore} from "../expiring-kv"; 2 | import {UserAuthenticationRole} from "./types"; 3 | 4 | const STORE_NAME = "userRole"; 5 | 6 | export function getUserAuthenticationRoleStore() { 7 | return getExpiringStore(STORE_NAME, { 8 | counter: false 9 | }) 10 | } -------------------------------------------------------------------------------- /src/data/durable-event/get-durable-event.ts: -------------------------------------------------------------------------------- 1 | import {DurableEventData} from "./types"; 2 | import {getDurableEventStore} from "./store"; 3 | 4 | export function getDurableEvent(event: DurableEventData) { 5 | if (!event.durableEventId) return undefined; 6 | const store = getDurableEventStore(event); 7 | return store.get(event.durableEventId); 8 | } -------------------------------------------------------------------------------- /src/static.ts: -------------------------------------------------------------------------------- 1 | export const NZCODE_URL = "https://www.nzcode.com/"; 2 | export const AXIOM_NZCODE_URL = 3 | "https://www.nzcode.com/shop/product/459221/axiom-700161/?variantId=1084232"; 4 | 5 | export const CC0_URL = 6 | "https://creativecommons.org/share-your-work/public-domain/cc0/"; 7 | export const MIT_URL = "https://snyk.io/learn/what-is-mit-license/"; 8 | -------------------------------------------------------------------------------- /src/data/durable-event/list-durable-events.ts: -------------------------------------------------------------------------------- 1 | import {getDurableEventStore} from "./store"; 2 | import {DurableEventTypeData} from "./types"; 3 | import {listKeyValueStoreIndex} from "../storage/store-index"; 4 | 5 | export function listDurableEvents(event: DurableEventTypeData) { 6 | const store = getDurableEventStore(event); 7 | return store.values(); 8 | } -------------------------------------------------------------------------------- /src/examples/add-partner.ts: -------------------------------------------------------------------------------- 1 | export const url = "/add-partner"; 2 | export const body = { 3 | partnerId: "1234", 4 | partnerName: "ABF Clinic", 5 | location: "Auckland", 6 | remote: true, 7 | onsite: true 8 | } 9 | export const options = { 10 | method: "POST", 11 | body 12 | }; 13 | export const response = { 14 | success: true 15 | }; -------------------------------------------------------------------------------- /src/data/durable-event/delete-durable-event.ts: -------------------------------------------------------------------------------- 1 | import {getDurableEventStore} from "./store"; 2 | import {DurableEvent, DurableEventData} from "./types"; 3 | 4 | export function deleteDurableEvent(event: DurableEventData) { 5 | const store = getDurableEventStore(event); 6 | if (!event.durableEventId) return undefined; 7 | return store.delete(event.durableEventId); 8 | } -------------------------------------------------------------------------------- /src/types/redis-lock.d.ts: -------------------------------------------------------------------------------- 1 | declare module "redis-lock" { 2 | import { RedisClientType } from "redis"; 3 | 4 | export interface UnlockFn { 5 | (): Promise; 6 | } 7 | 8 | export interface LockFn { 9 | (name: string): Promise 10 | } 11 | 12 | export default function lock(client: RedisClientType): LockFn 13 | 14 | } -------------------------------------------------------------------------------- /src/data/identifier/schema.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export const identifier = { 4 | type: "object", 5 | properties: { 6 | type: { 7 | type: "string", 8 | }, 9 | identifier: { 10 | type: "string", 11 | }, 12 | identifiedAt: { 13 | type: "string", 14 | } 15 | }, 16 | required: ["type", "identifier", "identifiedAt"], 17 | } as const; 18 | -------------------------------------------------------------------------------- /src/data/background/types.ts: -------------------------------------------------------------------------------- 1 | import {Expiring} from "../expiring"; 2 | 3 | 4 | export interface BackgroundData extends Expiring, Record { 5 | 6 | } 7 | 8 | export interface Background extends BackgroundData { 9 | backgroundKey: string; 10 | backgroundId: string; 11 | createdAt: string; 12 | expiresAt: string; 13 | completedAt?: string; 14 | } -------------------------------------------------------------------------------- /src/data/authorisation-notification/store.ts: -------------------------------------------------------------------------------- 1 | import {getKeyValueStore} from "../kv"; 2 | import {AuthorisationNotification} from "./types"; 3 | 4 | const STORE_NAME = "authorisationNotification" as const; 5 | 6 | export function getAuthorisationNotificationStore() { 7 | return getKeyValueStore(STORE_NAME, { 8 | counter: false 9 | }); 10 | } -------------------------------------------------------------------------------- /src/data/system-log/types.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface SystemLogData extends Record { 3 | uniqueCode?: string; 4 | value?: number; 5 | partnerId: string; 6 | message: string; 7 | timestamp?: string; 8 | action?: string; 9 | } 10 | 11 | export interface SystemLog extends SystemLogData { 12 | systemLogId: string; 13 | timestamp: string; 14 | } -------------------------------------------------------------------------------- /src/tests/worker/service-worker/loop/index.ts: -------------------------------------------------------------------------------- 1 | import {register} from "./registration"; 2 | 3 | const { a } = await register(); 4 | 5 | const responseA = await a.fetch("/", { 6 | method: "POST", 7 | body: JSON.stringify({ 8 | limit: 3 + Math.round(Math.random() * 6) 9 | }) 10 | }); 11 | 12 | const final = await responseA.json(); 13 | console.log(final); 14 | 15 | -------------------------------------------------------------------------------- /src/tests/worker/service-worker/store/store.ts: -------------------------------------------------------------------------------- 1 | try { 2 | await import("./delete"); 3 | await import("./get"); 4 | await import("./patch"); 5 | await import("./post"); 6 | await import("./put"); 7 | await import("./head"); 8 | } catch (error) { 9 | console.error("Error creating store service worker", error); 10 | throw error 11 | } 12 | 13 | export {}; -------------------------------------------------------------------------------- /src/data/user-credential/get-user-credential.ts: -------------------------------------------------------------------------------- 1 | import {getUserCredentialStore} from "./store"; 2 | import {UserCredential, UserCredentialIdentifiers} from "./types"; 3 | 4 | export async function getUserCredential(data: UserCredentialIdentifiers): Promise { 5 | const store = await getUserCredentialStore(data.userId); 6 | return store.get(data.userCredentialId); 7 | } -------------------------------------------------------------------------------- /src/data/file/list-files.ts: -------------------------------------------------------------------------------- 1 | import {getFileStore, getNamedFileStore} from "./store"; 2 | import {FileType} from "./types"; 3 | 4 | export function listFiles() { 5 | const store = getFileStore(); 6 | return store.values(); 7 | } 8 | 9 | export function listNamedFiles(type: FileType, typeId: string) { 10 | const store = getNamedFileStore(type, typeId); 11 | return store.values(); 12 | } -------------------------------------------------------------------------------- /src/tests/readme/cache/cache.js: -------------------------------------------------------------------------------- 1 | import { caches } from "@virtualstate/internal"; 2 | 3 | const cache = await caches.open("cache"); 4 | 5 | const url = "https://example.com"; 6 | 7 | await cache.add(url); 8 | 9 | const response = await cache.match(url); 10 | const text = await response.text(); 11 | 12 | console.log(response.status, text.substring(0, 15), text.length); // 200 ""; -------------------------------------------------------------------------------- /src/data/access-token/types.ts: -------------------------------------------------------------------------------- 1 | 2 | export type AccessTokenType = "partner" | "discord" 3 | 4 | export interface AccessTokenData extends Record { 5 | partnerId?: string; 6 | accessTokenType?: AccessTokenType | string; 7 | } 8 | 9 | export interface AccessToken extends AccessTokenData { 10 | accessToken: string; 11 | createdAt: string; 12 | disabledAt?: string; 13 | } -------------------------------------------------------------------------------- /src/data/happening/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./store"; 3 | export * from "./add-happening"; 4 | export * from "./set-happening"; 5 | export * from "./get-happening-tree"; 6 | export * from "./get-happening"; 7 | export * from "./delete-happening"; 8 | export * from "./list-happening-trees"; 9 | export * from "./list-happenings"; 10 | export * as happeningSchema from "./schema"; -------------------------------------------------------------------------------- /src/data/change/store.ts: -------------------------------------------------------------------------------- 1 | import {Change, ChangeTargetIdentifier} from "./types"; 2 | import {getExpiringStore} from "../expiring-kv"; 3 | 4 | const STORE_NAME = "change" as const; 5 | 6 | export function getChangeStore({ target , type }: ChangeTargetIdentifier ) { 7 | return getExpiringStore(STORE_NAME, { 8 | counter: true, 9 | prefix: `change:${type}:target:${target.type}` 10 | }); 11 | } -------------------------------------------------------------------------------- /src/listen/file/index.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance } from "fastify"; 2 | import { getFileImageWatermarkRoutes } from "./get-file-image-watermark"; 3 | 4 | export async function fileRoutes(fastify: FastifyInstance) { 5 | async function routes(fastify: FastifyInstance) { 6 | fastify.register(getFileImageWatermarkRoutes); 7 | } 8 | 9 | fastify.register(routes, { 10 | prefix: "/files", 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /src/data/cache/store.ts: -------------------------------------------------------------------------------- 1 | import { Cached } from "./types"; 2 | import { DAY_MS, getExpiringStore } from "../expiring-kv"; 3 | 4 | export const DEFAULT_EXPIRES_IN_MS = 0.25 * DAY_MS; 5 | export const PAGE_EXPIRES_IN_MS = 0.25 * DAY_MS; 6 | 7 | const STORE_NAME = "cache" as const; 8 | 9 | export function getCacheStore() { 10 | return getExpiringStore>(STORE_NAME, { 11 | counter: false, 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /src/listen/system-log/index.ts: -------------------------------------------------------------------------------- 1 | import {FastifyInstance} from "fastify"; 2 | import {listSystemLogsRoutes} from "./list-system-logs"; 3 | 4 | export async function systemLogRoutes(fastify: FastifyInstance) { 5 | 6 | async function routes(fastify: FastifyInstance) { 7 | fastify.register(listSystemLogsRoutes); 8 | } 9 | 10 | fastify.register(routes, { 11 | prefix: "/system-logs" 12 | }); 13 | 14 | } -------------------------------------------------------------------------------- /src/data/attendee/types.ts: -------------------------------------------------------------------------------- 1 | export interface AttendeeData extends Record { 2 | reference: string; 3 | name?: string; 4 | email?: string; 5 | } 6 | 7 | export interface Attendee extends AttendeeData { 8 | attendeeId: string; 9 | createdAt: string; 10 | createdByPartnerId?: string; 11 | createdByUserId?: string; 12 | } 13 | 14 | export type PartialAttendee = AttendeeData & Partial; -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make by way of an issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | We are open to all ideas big or small, and are greatly appreciative of any and all contributions. 7 | 8 | Please note we have a code of conduct, please follow it in all your interactions with the project. 9 | -------------------------------------------------------------------------------- /src/react/client/utils.ts: -------------------------------------------------------------------------------- 1 | export function ok(value: unknown, message?: string): asserts value; 2 | export function ok(value: unknown, message?: string): asserts value is T; 3 | export function ok(value: unknown, message?: string): asserts value { 4 | if (!value) { 5 | throw new Error(message ?? "Expected value"); 6 | } 7 | } 8 | 9 | export function isLike(value: unknown): value is T { 10 | return !!value; 11 | } 12 | -------------------------------------------------------------------------------- /src/tests/worker/service-worker/routes.worker.ts: -------------------------------------------------------------------------------- 1 | import {requestMethod} from "./routes.example"; 2 | 3 | requestMethod.get("/test", () => { 4 | console.log("In get handler"); 5 | return new Response("Hello from test get handler"); 6 | }) 7 | 8 | requestMethod.put("/test", async (request) => { 9 | console.log("In put handler"); 10 | return new Response(request.body, { 11 | headers: request.headers 12 | }); 13 | }) -------------------------------------------------------------------------------- /src/data/file/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./add-file"; 2 | export * from "./get-file"; 3 | export * from "./list-files"; 4 | export * from "./set-file"; 5 | export * from "./store"; 6 | export * from "./types"; 7 | export * as fileSchema from "./schema"; 8 | export * from "./r2"; 9 | export * from "./source"; 10 | export * from "./disk"; 11 | export * from "./resolve-files"; 12 | export * from "./resolve-file"; 13 | export * from "./save"; 14 | export * from "./fetch"; -------------------------------------------------------------------------------- /src/data/organisation/add-organisation.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from "uuid"; 2 | import { OrganisationData, Organisation } from "./types"; 3 | import { setOrganisation } from "./set-organisation"; 4 | 5 | export async function addOrganisation(data: OrganisationData): Promise { 6 | return setOrganisation({ 7 | ...data, 8 | organisationId: v4(), 9 | approved: false, 10 | approvedAt: undefined, 11 | approvedByUserId: undefined 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /src/listen/branding/palette.ts: -------------------------------------------------------------------------------- 1 | import {FastifyInstance} from "fastify"; 2 | import {getBrandingPalette} from "../../branding"; 3 | 4 | export async function paletteRoutes(fastify: FastifyInstance) { 5 | try { 6 | fastify.get("/palette", { 7 | async handler(request, response) { 8 | const palette = await getBrandingPalette(); 9 | response.send(palette); 10 | } 11 | }) 12 | } catch {} 13 | 14 | } -------------------------------------------------------------------------------- /src/listen/user-credential/index.ts: -------------------------------------------------------------------------------- 1 | import {FastifyInstance} from "fastify"; 2 | import {deleteUserCredentialRoutes} from "./delete-user-credential"; 3 | 4 | export async function userCredentialRoutes(fastify: FastifyInstance) { 5 | 6 | async function routes(fastify: FastifyInstance) { 7 | fastify.register(deleteUserCredentialRoutes); 8 | } 9 | 10 | fastify.register(routes, { 11 | prefix: "/users/:userId/credentials" 12 | }); 13 | 14 | } -------------------------------------------------------------------------------- /src/worker/service-worker/service-worker-config.ts: -------------------------------------------------------------------------------- 1 | import {DurableServiceWorkerScope} from "./types"; 2 | import {SERVICE_WORKER_ID} from "../../config"; 3 | 4 | declare var self: DurableServiceWorkerScope; 5 | 6 | export function getServiceWorkerId() { 7 | if (typeof self !== "undefined") { 8 | if (self.registration?.durable) { 9 | return self.registration.durable.serviceWorkerId; 10 | } 11 | } 12 | return SERVICE_WORKER_ID; 13 | } -------------------------------------------------------------------------------- /src/worker/service-worker/virtual.ts: -------------------------------------------------------------------------------- 1 | import {virtual} from "../../events/virtual/virtual"; 2 | 3 | export async function * generateVirtualServiceWorkerEvents() { 4 | // const store = getServiceWorkerRegistrationStore(); 5 | // for await (const { serviceWorkerId } of store) { 6 | // // TODO Generate per service worker some events :) 7 | // } 8 | } 9 | 10 | export const removeServiceWorkerVirtualFunction = virtual(generateVirtualServiceWorkerEvents); 11 | -------------------------------------------------------------------------------- /src/data/happening/store.ts: -------------------------------------------------------------------------------- 1 | import {Happening, HappeningData} from "./types"; 2 | import {KeyValueStoreOptions} from "../storage"; 3 | import {getExpiringStore} from "../expiring-kv"; 4 | 5 | const STORE_NAME = "happening" as const; 6 | 7 | export function getHappeningStore(name: string = STORE_NAME, options?: KeyValueStoreOptions) { 8 | return getExpiringStore(name, { 9 | counter: true, 10 | ...options 11 | }); 12 | } -------------------------------------------------------------------------------- /src/data/system-log/log.ts: -------------------------------------------------------------------------------- 1 | import {v4} from "uuid"; 2 | import {SystemLogData} from "./types"; 3 | import {getSystemLogStore} from "./store"; 4 | 5 | export async function log(document: SystemLogData) { 6 | const store = getSystemLogStore(); 7 | const systemLogId = v4(); 8 | await store.set(systemLogId, { 9 | ...document, 10 | timestamp: document.timestamp ?? new Date().toISOString(), 11 | systemLogId 12 | }); 13 | return systemLogId 14 | } -------------------------------------------------------------------------------- /src/listen/branding/index.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance } from "fastify"; 2 | import {logoRoutes} from "./logo"; 3 | import {paletteRoutes} from "./palette"; 4 | 5 | export async function brandingRoutes(fastify: FastifyInstance) { 6 | async function routes(fastify: FastifyInstance) { 7 | fastify.register(logoRoutes); 8 | fastify.register(paletteRoutes); 9 | } 10 | 11 | fastify.register(routes, { 12 | prefix: "/branding", 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /src/react/server/paths/logout.tsx: -------------------------------------------------------------------------------- 1 | import { FastifyReply, FastifyRequest } from "fastify"; 2 | 3 | export const path = "/logout"; 4 | export const anonymous = true; 5 | 6 | export async function handler(request: FastifyRequest, response: FastifyReply) { 7 | response.header("Location", "/api/authentication/logout"); 8 | response.status(302); 9 | } 10 | 11 | export function Logout() { 12 | return

Logging out! Redirecting...

; 13 | } 14 | 15 | export const Component = Logout; -------------------------------------------------------------------------------- /src/react/server/paths/views.ts: -------------------------------------------------------------------------------- 1 | export * as invite from "./invite" 2 | export * as userCredential from "./user-credential" 3 | export * as durableEvent from "./durable-event"; 4 | 5 | export * as SettingsView from "./settings"; 6 | export * as HomeView from "./home"; 7 | export * as IndexView from "./home-index"; 8 | export * as FeedbackView from "./feedback"; 9 | export * as LoginView from "./login"; 10 | export * as LogoutView from "./logout"; 11 | export * as ErrorsView from "./error"; -------------------------------------------------------------------------------- /public/example-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/data/authorisation-notification/types.ts: -------------------------------------------------------------------------------- 1 | export type AuthorisationNotificationType = ( 2 | | "payment" 3 | | "message" 4 | ); 5 | 6 | export interface AuthorisationNotificationData extends Record { 7 | type: AuthorisationNotificationType; 8 | } 9 | 10 | export interface AuthorisationNotification extends AuthorisationNotificationData { 11 | notificationId: string; 12 | authorisationId: string; 13 | createdAt: string; 14 | stateId?: string; 15 | } -------------------------------------------------------------------------------- /src/data/task/create-task-happening-context.ts: -------------------------------------------------------------------------------- 1 | import {createGetHappeningTreeContext, CreateGetHappeningTreeContextOptions} from "../happening"; 2 | import {Task} from "./types"; 3 | import {getTask} from "./get-task"; 4 | 5 | export function createTaskHappeningTreeContext(options?: CreateGetHappeningTreeContextOptions) { 6 | return createGetHappeningTreeContext({ 7 | get: getTask, 8 | ...options, 9 | idKey: "taskId", 10 | }); 11 | } -------------------------------------------------------------------------------- /src/data/durable-request/is.ts: -------------------------------------------------------------------------------- 1 | import {DurableBody} from "./types"; 2 | import {isLike} from "../../is"; 3 | 4 | export function isDurableBody(value: unknown): value is DurableBody { 5 | return !!( 6 | isLike(value) && 7 | typeof value.type === "string" && 8 | ( 9 | value.type === "file" || 10 | value.type === "base64" || 11 | value.type === "cache" || 12 | value.type === "source" 13 | ) 14 | ) 15 | } -------------------------------------------------------------------------------- /src/package.readonly.ts: -------------------------------------------------------------------------------- 1 | // File generated by scripts/git-info-build.js 2 | 3 | export const commit = ""; 4 | export const commitShort = ""; 5 | export const commitAuthor = ""; 6 | export const commitEmail = ""; 7 | export const commitMessage = ""; 8 | export const commitAt = ""; 9 | 10 | // Variables to be replaced after tests 11 | export const secondsBetweenCommitAndTestCompletion = ""; 12 | export const minutesBetweenCommitAndTestCompletion = ""; 13 | export const timeBetweenCommitAndTestCompletion = ""; -------------------------------------------------------------------------------- /src/worker/service-worker/constants.ts: -------------------------------------------------------------------------------- 1 | export const WORKER_INITIATED = "worker:internal:initiated" 2 | export const WORKER_MESSAGE = "worker:internal:message" 3 | export const WORKER_ITERATE = "worker:internal:iterate" 4 | export const WORKER_YIELD = "worker:internal:yield" 5 | export const WORKER_BREAK = "worker:internal:break" 6 | export const WORKER_TERMINATE = "worker:internal:terminate" 7 | export const WORKER_ERROR = "worker:internal:error" 8 | 9 | export const WORKER_ADD_CONTEXT = "worker:internal:context:add"; -------------------------------------------------------------------------------- /src/react/client/pages/paths/home.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom/client"; 2 | 3 | function Home() { 4 | return ( 5 |
6 | Page loaded at {new Date().toISOString()} 7 |
8 | ) 9 | } 10 | 11 | export async function home() { 12 | 13 | console.log("Hello from home!"); 14 | 15 | const element = document.getElementById("react-home"); 16 | if (!element) return; 17 | const root = ReactDOM.createRoot(element); 18 | root.render(); 19 | 20 | } -------------------------------------------------------------------------------- /src/data/durable-request/store.ts: -------------------------------------------------------------------------------- 1 | import {getExpiringStore} from "../expiring-kv"; 2 | import {DurableRequest} from "./types"; 3 | 4 | const STORE_NAME = "request" as const; 5 | 6 | export interface DurableRequestStoreOptions { 7 | name?: string; 8 | prefix?: string; 9 | } 10 | 11 | export function getDurableRequestStore({ name, prefix }: DurableRequestStoreOptions = {}) { 12 | return getExpiringStore(name || STORE_NAME, { 13 | counter: false, 14 | prefix 15 | }); 16 | } -------------------------------------------------------------------------------- /src/data/task/types.ts: -------------------------------------------------------------------------------- 1 | import {HappeningData} from "../happening"; 2 | 3 | export type TaskType = 4 | | "inventory" 5 | | "packing" 6 | | "picking" 7 | | "order" 8 | | "product" 9 | | "offer" 10 | 11 | export interface TaskData extends HappeningData { 12 | type: TaskType 13 | title: string; 14 | organisationId?: string; 15 | attendees: string[]; 16 | } 17 | 18 | export interface Task extends TaskData { 19 | taskId: string; 20 | createdAt: string; 21 | updatedAt: string; 22 | } 23 | -------------------------------------------------------------------------------- /src/tests/worker/service-worker/store/put.ts: -------------------------------------------------------------------------------- 1 | import {requestMethod} from "../routes.example"; 2 | import {cache} from "./cache"; 3 | 4 | requestMethod.put({ pathname: "/:type/:id" }, async request => { 5 | await cache.put(request.url, new Response(request.body, { 6 | headers: { 7 | "Content-Type": request.headers.get("Content-Type"), 8 | "Last-Modified": new Date().toUTCString() 9 | } 10 | })); 11 | return new Response(null, { 12 | status: 204 13 | }); 14 | }) -------------------------------------------------------------------------------- /src/worker/service-worker/pool-worker.ts: -------------------------------------------------------------------------------- 1 | import { ThreadWorker } from "poolifier"; 2 | import {onServiceWorkerWorkerData} from "./worker"; 3 | 4 | export interface ServiceWorkerWorkerData { 5 | serviceWorkerId: string; 6 | } 7 | 8 | class ServiceWorkerWorker extends ThreadWorker { 9 | constructor() { 10 | super(data => onServiceWorkerWorkerData(data), { 11 | maxInactiveTime: 120000 12 | }); 13 | } 14 | } 15 | 16 | export default new ServiceWorkerWorker(); -------------------------------------------------------------------------------- /src/data/file/delete-file.ts: -------------------------------------------------------------------------------- 1 | import {getFileStore, resolveFileStore} from "./store"; 2 | import {File, FileData} from "./types"; 3 | import {KeyValueStore} from "../storage"; 4 | 5 | export function deleteFile(file: string | FileData, givenStore?: KeyValueStore) { 6 | if (typeof file === "string") { 7 | const store = getFileStore(); 8 | return store.delete(file); 9 | } else { 10 | const store = resolveFileStore(file, givenStore); 11 | return store.delete(file.fileId); 12 | } 13 | } -------------------------------------------------------------------------------- /src/data/system-log/list-system-logs.ts: -------------------------------------------------------------------------------- 1 | import {getSystemLogStore} from "./store"; 2 | import {SystemLog} from "./types"; 3 | 4 | export interface RetrieveSystemLogsInput { 5 | partnerId?: string 6 | } 7 | 8 | export async function listSystemLogs({ partnerId }: RetrieveSystemLogsInput): Promise { 9 | if (!partnerId) return []; 10 | const store = await getSystemLogStore(); 11 | const values = await store.values(); 12 | return values.filter(value => value.partnerId === partnerId || !value.partnerId); 13 | } -------------------------------------------------------------------------------- /src/listen/serverless.ts: -------------------------------------------------------------------------------- 1 | // It appears vercel serverless requires strong references 2 | // for inclusion in the file system 3 | import "../references"; 4 | 5 | import * as dotenv from "dotenv"; 6 | dotenv.config(); 7 | 8 | const { createFastifyApplication } = await import("./listen"); 9 | 10 | const app = await createFastifyApplication(); 11 | 12 | export default async function handler(request: unknown, response: unknown) { 13 | await app.ready(); 14 | app.server.emit('request', request, response); 15 | } 16 | 17 | export { handler } -------------------------------------------------------------------------------- /src/react/server/paths/branding/index.tsx: -------------------------------------------------------------------------------- 1 | import {getR2URLFileData, getSigned, getSignedUrl, R2SignedURL} from "../../../../data"; 2 | import {useInput} from "../../data"; 3 | 4 | export const path = "/settings/branding"; 5 | export const trusted = true; 6 | 7 | interface Input { 8 | 9 | } 10 | 11 | export async function handler(): Promise { 12 | 13 | return { 14 | 15 | } as const 16 | } 17 | 18 | export function Component() { 19 | const { } = useInput() 20 | return ( 21 |
22 | ) 23 | } -------------------------------------------------------------------------------- /src/data/file/fetch.ts: -------------------------------------------------------------------------------- 1 | import {FileData} from "./types"; 2 | import {isR2, readFileFromR2} from "./r2"; 3 | import {readFileFromDisk} from "./disk"; 4 | 5 | export async function readFile(file: FileData) { 6 | if (isR2()) { 7 | return readFileFromR2(file); 8 | } else { 9 | return readFileFromDisk(file); 10 | } 11 | } 12 | 13 | export async function fetchFile(file: FileData) { 14 | const contents = await readFile(file); 15 | return new Response(contents, { 16 | status: contents ? 200 : 404 17 | }); 18 | } -------------------------------------------------------------------------------- /src/data/partner/list-partners.ts: -------------------------------------------------------------------------------- 1 | import { Partner } from "./types"; 2 | import { getPartnerStore } from "./store"; 3 | 4 | export interface ListPartnersInput { 5 | authorizedPartnerId?: string; 6 | } 7 | 8 | export async function listPartners({ 9 | authorizedPartnerId, 10 | }: ListPartnersInput = {}): Promise { 11 | const store = getPartnerStore(); 12 | const partners = await store.values(); 13 | return partners.filter( 14 | (partner) => partner.approvedAt || partner.partnerId === authorizedPartnerId 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/data/user-credential/store.ts: -------------------------------------------------------------------------------- 1 | import { getKeyValueStore } from "../kv"; 2 | import { UserCredential } from "./types"; 3 | import {DEFAULT_USER_EXPIRES_IN_MS} from "../user"; 4 | 5 | export const DEFAULT_CREDENTIAL_EXPIRES_IN_MS = DEFAULT_USER_EXPIRES_IN_MS; 6 | 7 | const STORE_NAME = "userCredential" as const; 8 | 9 | export function getUserCredentialStore(userId: string) { 10 | return getKeyValueStore(STORE_NAME, { 11 | counter: false, 12 | // Partition by userId 13 | prefix: `userId::${userId}::` 14 | }); 15 | } -------------------------------------------------------------------------------- /src/data/storage/redis-client-helpers.ts: -------------------------------------------------------------------------------- 1 | import {KeyValueStoreOptions} from "./types"; 2 | 3 | export function isRedis() { 4 | return !!getRedisUrl(); 5 | } 6 | 7 | export function getRedisUrl() { 8 | return process.env.REDIS_URL; 9 | } 10 | 11 | export function getRedisPrefix(name: string, options?: KeyValueStoreOptions) { 12 | return `${name}::${options?.prefix ?? ""}`; 13 | } 14 | 15 | export function getRedisPrefixedKey(name: string, key: string, options?: KeyValueStoreOptions): string { 16 | return `${getRedisPrefix(name, options)}${key}`; 17 | } -------------------------------------------------------------------------------- /src/worker/service-worker/dispatchers/activate.ts: -------------------------------------------------------------------------------- 1 | import {dispatcher} from "../../../events/schedule/schedule"; 2 | import {createWaitUntil, ExtendableEvent} from "../../../fetch"; 3 | 4 | export interface ActivateEvent extends ExtendableEvent { 5 | 6 | } 7 | 8 | export const removeActivateDispatcher = dispatcher("activate", async (event, dispatch) => { 9 | const { 10 | wait, 11 | waitUntil 12 | } = createWaitUntil(event); 13 | await dispatch({ 14 | ...event, 15 | waitUntil 16 | }); 17 | await wait(); 18 | }); -------------------------------------------------------------------------------- /src/react/client/components/icons/svg-text.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from "./types"; 2 | 3 | export interface SvgTextIconProps extends IconProps { 4 | text: string; 5 | } 6 | 7 | export function SvgTextIcon({ text, ...props }: SvgTextIconProps) { 8 | return ( 9 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/data/form-meta/schema.ts: -------------------------------------------------------------------------------- 1 | export const formMetaData = { 2 | type: "object", 3 | properties: {}, 4 | additionalProperties: true, 5 | required: [], 6 | } as const; 7 | 8 | export const formMeta = { 9 | type: "object", 10 | properties: { 11 | formMetaId: { 12 | type: "string", 13 | }, 14 | ...formMetaData.properties, 15 | createdAt: { 16 | type: "string", 17 | }, 18 | updatedAt: { 19 | type: "string", 20 | }, 21 | }, 22 | required: [...formMetaData.required, "formMetaId", "createdAt", "updatedAt"], 23 | } as const; 24 | -------------------------------------------------------------------------------- /src/data/partner/types.ts: -------------------------------------------------------------------------------- 1 | import { OrganisationBaseData } from "../organisation"; 2 | 3 | export interface PartnerData extends Record { 4 | partnerName: string; 5 | countryCode?: string; 6 | } 7 | 8 | export interface AddPartnerData extends PartnerData, OrganisationBaseData {} 9 | 10 | export interface Partner extends PartnerData { 11 | partnerId: string; 12 | organisationId: string; 13 | accessToken?: string; 14 | createdAt: string; 15 | updatedAt: string; 16 | approved?: boolean; 17 | approvedAt?: string; 18 | approvedByUserId?: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/data/task/set-task.ts: -------------------------------------------------------------------------------- 1 | import { Task, TaskData } from "./types"; 2 | import { getTaskStore } from "./store"; 3 | import {v4} from "uuid"; 4 | 5 | export async function setTask( 6 | data: TaskData & Partial 7 | ): Promise { 8 | const store = await getTaskStore(); 9 | const updatedAt = new Date().toISOString(); 10 | const taskId = data.taskId || v4(); 11 | const document: Task = { 12 | createdAt: data.createdAt || updatedAt, 13 | ...data, 14 | taskId, 15 | updatedAt, 16 | }; 17 | await store.set(data.taskId, document); 18 | return document; 19 | } -------------------------------------------------------------------------------- /src/examples/retrive-system-logs.ts: -------------------------------------------------------------------------------- 1 | import type {SystemLogData} from "../data"; 2 | 3 | export const url = "/system-logs"; 4 | export const options = { 5 | method: "GET" 6 | }; 7 | export const response: SystemLogData[] = [ 8 | { 9 | timestamp: "2022-05-01T10:30:00Z", 10 | message: "Unique code generated", 11 | uniqueCode: "ABC123", 12 | partnerId: "1234", 13 | }, 14 | { 15 | timestamp: "2022-05-01T11:00:00Z", 16 | message: "Unique code redeemed", 17 | uniqueCode: "ABC123", 18 | partnerId: "1234", 19 | }, 20 | ] -------------------------------------------------------------------------------- /src/react/client/components/icons/clock.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from "./types"; 2 | 3 | export function ClockIcon(props: IconProps) { 4 | return ( 5 | 14 | 15 | 16 | 17 | ); 18 | } -------------------------------------------------------------------------------- /src/tests/readme/worker/main.js: -------------------------------------------------------------------------------- 1 | import { serviceWorker, createServiceWorkerFetch } from "@virtualstate/internal"; 2 | import { fileURLToPath } from "node:url"; 3 | import { dirname, join } from "node:path"; 4 | 5 | const pathname = fileURLToPath(import.meta.url); 6 | const worker = join(dirname(pathname), "./worker.js"); 7 | 8 | const registration = await serviceWorker.register(worker); 9 | 10 | const fetch = createServiceWorkerFetch(registration); 11 | 12 | const response = await fetch("/"); 13 | const text = await response.text(); 14 | 15 | console.log(response.status, text); // 200 "Hello"; -------------------------------------------------------------------------------- /public/example-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/react/client/components/icons/lightning-bolt.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from "./types"; 2 | 3 | export function LightningBoltIcon(props: IconProps) { 4 | return ( 5 | 14 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/react/client/components/icons/x-circle.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from "./types"; 2 | 3 | export function XCircleIcon(props: IconProps) { 4 | return ( 5 | 14 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/tests/worker/service-worker/store/head.ts: -------------------------------------------------------------------------------- 1 | import {requestMethod} from "../routes.example"; 2 | import {cache} from "./cache"; 3 | 4 | requestMethod.head({ pathname: "/:type/:id" }, async request => { 5 | const match = await cache.match(request); 6 | return new Response(null, { 7 | status: match ? 200 : 404, 8 | headers: { 9 | "Content-Type": match?.headers.get("Content-Type"), 10 | "Last-Modified": ( 11 | match?.headers.get("Last-Modified") ?? 12 | match?.headers.get("Date") 13 | ) 14 | } 15 | }); 16 | }) -------------------------------------------------------------------------------- /src/data/cache/types.ts: -------------------------------------------------------------------------------- 1 | import { Expiring } from "../expiring"; 2 | 3 | type CachingType = "counter" | "expiring"; 4 | 5 | export interface CacheData extends Expiring { 6 | value: T; 7 | key: string; 8 | type?: CachingType; 9 | role?: boolean; 10 | // Indicates that the key is stable and does not need to be 11 | // reset over package revisions 12 | stable?: boolean; 13 | } 14 | 15 | export interface Cached extends CacheData { 16 | type: CachingType; 17 | roles: string[]; 18 | counter: number; 19 | counterType: "store" | "global"; 20 | createdAt: string; 21 | } 22 | -------------------------------------------------------------------------------- /src/data/file/get-file.ts: -------------------------------------------------------------------------------- 1 | import {getFileStore, getNamedFileStore, resolveFileStore} from "./store"; 2 | import {FileData, FileType} from "./types"; 3 | 4 | export function getFile(fileId: string | FileData) { 5 | if (typeof fileId === "string") { 6 | const store = getFileStore(); 7 | return store.get(fileId); 8 | } else { 9 | const store = resolveFileStore(fileId); 10 | return store.get(fileId.fileId); 11 | } 12 | } 13 | 14 | export function getNamedFile(type: FileType, typeId: string, fileId: string) { 15 | const store = getNamedFileStore(type, typeId); 16 | return store.get(fileId); 17 | } -------------------------------------------------------------------------------- /src/react/client/components/icons/magnifying-glass.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from "./types"; 2 | 3 | export function MagnifyingGlassIcon(props: IconProps) { 4 | return ( 5 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/react/server/paths/error.tsx: -------------------------------------------------------------------------------- 1 | import { useError } from "../data"; 2 | 3 | export const path = "/error"; 4 | 5 | export function Errors() { 6 | const error = useError(); 7 | console.error(error); 8 | return ( 9 | <> 10 |

Ran into an error!

11 |

{error instanceof Error ? error.message : String(error)}

12 |
13 |
14 | 18 | Go to home page 19 | 20 | 21 | ); 22 | } 23 | 24 | export const Component = Errors; -------------------------------------------------------------------------------- /src/authentication/authsignal.ts: -------------------------------------------------------------------------------- 1 | import { Authsignal } from "@authsignal/node"; 2 | 3 | export const { 4 | AUTHSIGNAL_TENANT, 5 | AUTHSIGNAL_KEY, 6 | AUTHSIGNAL_SECRET, 7 | AUTHSIGNAL_REDIRECT_URL, 8 | AUTHSIGNAL_WEBAUTHN 9 | } = process.env; 10 | 11 | export const AUTHSIGNAL_BASE_URL = process.env.AUTHSIGNAL_API_URL || "https://au.signal.authsignal.com/v1"; 12 | export const AUTHSIGNAL_CHALLENGE_API_URL = process.env.AUTHSIGNAL_CHALLENGE_API_URL || "https://au.api.authsignal.com/v1"; 13 | 14 | export const authsignal = new Authsignal({ 15 | secret: process.env.AUTHSIGNAL_SECRET, 16 | apiBaseUrl: AUTHSIGNAL_BASE_URL, 17 | }); -------------------------------------------------------------------------------- /src/react/client/components/icons/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./calendar"; 2 | export * from "./globe"; 3 | export * from "./x-circle"; 4 | export * from "./annotation"; 5 | export * from "./globe-alt"; 6 | export * from "./lightning-bolt"; 7 | export * from "./scale"; 8 | export * from "./eye-dropper"; 9 | export * from "./wrench"; 10 | export * from "./prescription-bottle"; 11 | export * from "./svg-text"; 12 | export * from "./trash"; 13 | export * from "./bag"; 14 | export * from "./magnifying-glass"; 15 | export * from "./calendar-days"; 16 | export * from "./credit-card"; 17 | export * from "./user-circle"; 18 | export * from "./clock"; -------------------------------------------------------------------------------- /src/worker/service-worker/import.ts: -------------------------------------------------------------------------------- 1 | import {serviceWorker} from "./container"; 2 | import {onServiceWorkerWorkerData} from "./worker"; 3 | 4 | /** 5 | * Use this function to import a service worker script into the global scope. 6 | * 7 | * Only one service worker is expected to be imported in a global scope. 8 | * 9 | * @param url 10 | */ 11 | export async function importServiceWorker(url: string) { 12 | const registration = await serviceWorker.register(url); 13 | 14 | await onServiceWorkerWorkerData({ 15 | serviceWorkerId: registration.durable.serviceWorkerId 16 | }); 17 | 18 | return registration; 19 | } -------------------------------------------------------------------------------- /scripts/imported/index.js: -------------------------------------------------------------------------------- 1 | /* c8 ignore start */ 2 | 3 | const initialImportPath = 4 | getConfig()["@virtualstate/app-history/test/imported/path"] ?? 5 | "@virtualstate/app-history"; 6 | 7 | if (typeof initialImportPath !== "string") 8 | throw new Error("Expected string import path"); 9 | 10 | export const { AppHistory } = await import(initialImportPath); 11 | 12 | export function getConfig() { 13 | return { 14 | ...getNodeConfig(), 15 | }; 16 | } 17 | 18 | function getNodeConfig() { 19 | if (typeof process === "undefined") return {}; 20 | return JSON.parse(process.env.TEST_CONFIG ?? "{}"); 21 | } 22 | /* c8 ignore end */ 23 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/example-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/data/authentication-role/set-role.ts: -------------------------------------------------------------------------------- 1 | import {PartialUserAuthenticationRole, UserAuthenticationRole, UserAuthenticationRoleData} from "./types"; 2 | import {getUserAuthenticationRoleStore} from "./store"; 3 | import {v4} from "uuid"; 4 | 5 | export async function setUserAuthenticationRole(data: PartialUserAuthenticationRole) { 6 | const store = getUserAuthenticationRoleStore(); 7 | const updatedAt = new Date().toISOString() 8 | const role: UserAuthenticationRole = { 9 | ...data, 10 | createdAt: data.createdAt || updatedAt, 11 | updatedAt 12 | }; 13 | await store.set(role.userId, role); 14 | return role; 15 | } -------------------------------------------------------------------------------- /src/data/happening/list-happenings.ts: -------------------------------------------------------------------------------- 1 | import {getHappeningStore} from "./store"; 2 | 3 | export interface ListHappeningInput { 4 | type?: string | string[] 5 | } 6 | 7 | export async function listHappenings(options?: ListHappeningInput) { 8 | const types = Array.isArray(options.type) ? options.type : (options.type?.length ? [options.type] : []); 9 | const store = getHappeningStore(); 10 | let values = await store.values(); 11 | // TODO filter using indexes rather than filtering in memory 12 | if (types.length) { 13 | values = values.filter(happening => types.includes(happening.type)); 14 | } 15 | return values; 16 | } -------------------------------------------------------------------------------- /src/react/server/paths/index.ts: -------------------------------------------------------------------------------- 1 | import {View} from "../../../view"; 2 | 3 | import { 4 | SettingsView, 5 | HomeView, 6 | IndexView, 7 | FeedbackView, 8 | LogoutView, 9 | LoginView, 10 | ErrorsView, 11 | invite, 12 | userCredential, 13 | durableEvent 14 | } from "./views"; 15 | 16 | export * as namedViews from "./views"; 17 | 18 | export const views: View[] = [ 19 | SettingsView, 20 | HomeView, 21 | IndexView, 22 | FeedbackView, 23 | LoginView, 24 | LogoutView, 25 | ErrorsView, 26 | invite.create, 27 | invite.accept, 28 | userCredential.list, 29 | durableEvent.create, 30 | durableEvent.list, 31 | ]; 32 | 33 | export * from "./types"; -------------------------------------------------------------------------------- /src/tests/worker/service-worker/configured/prices.ts: -------------------------------------------------------------------------------- 1 | import {requestMethod} from "../routes.example"; 2 | 3 | export interface ProductPrice { 4 | productId: string; 5 | value: number; 6 | } 7 | 8 | const prices: ProductPrice[] = [ 9 | { 10 | productId: "apples", 11 | value: 1.2 12 | }, 13 | { 14 | productId: "pears", 15 | value: 1.5 16 | }, 17 | { 18 | productId: "limes", 19 | value: 4 20 | } 21 | ] 22 | 23 | console.log("Inside prices service worker!") 24 | 25 | requestMethod.get({ pathname: "/prices" }, () => { 26 | console.log("Inside prices fetch") 27 | return Response.json(prices) 28 | }) -------------------------------------------------------------------------------- /src/data/organisation/list-organisations.ts: -------------------------------------------------------------------------------- 1 | import { Organisation } from "./types"; 2 | import { getOrganisationStore } from "./store"; 3 | 4 | export interface ListOrganisationsInput { 5 | authorizedOrganisationId?: string; 6 | } 7 | 8 | export async function listOrganisations({ 9 | authorizedOrganisationId, 10 | }: ListOrganisationsInput = {}): Promise { 11 | const store = getOrganisationStore(); 12 | const organisations = await store.values(); 13 | return organisations.filter( 14 | (organisation) => 15 | organisation.approvedAt || 16 | organisation.organisationId === authorizedOrganisationId 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/react/client/components/checkout/checkout-empty.tsx: -------------------------------------------------------------------------------- 1 | import {MagnifyingGlassIcon} from "../icons"; 2 | 3 | export function CheckoutEmpty() { 4 | return ( 5 | 9 | 12 | Browse products 13 | 14 | ) 15 | } -------------------------------------------------------------------------------- /src/data/change/set-change.ts: -------------------------------------------------------------------------------- 1 | import { Change, ChangeData } from "./types"; 2 | import { getChangeStore } from "./store"; 3 | import {v4} from "uuid"; 4 | 5 | export async function setChange( 6 | data: ChangeData & Partial 7 | ): Promise { 8 | const updatedAt = new Date().toISOString(); 9 | const changeId = data.changeId || v4(); 10 | const status = data.status || "pending"; 11 | const document: Change = { 12 | createdAt: data.createdAt || updatedAt, 13 | changeId, 14 | ...data, 15 | status, 16 | updatedAt, 17 | }; 18 | const store = await getChangeStore(document); 19 | await store.set(data.changeId, document); 20 | return document; 21 | } -------------------------------------------------------------------------------- /src/events/virtual/index.ts: -------------------------------------------------------------------------------- 1 | import { generateVirtualEvents } from "./virtual"; 2 | import { on } from "../schedule"; 3 | import { limited } from "../../limited"; 4 | import { dispatchScheduledDurableEvents } from "../schedule/dispatch-scheduled"; 5 | 6 | export * from "./virtual"; 7 | 8 | const VIRTUAL = "virtual" as const; 9 | 10 | export async function onVirtualEvent() { 11 | for await (const events of generateVirtualEvents()) { 12 | await limited( 13 | events.map(event => () => dispatchScheduledDurableEvents({ 14 | event 15 | })) 16 | ); 17 | } 18 | } 19 | 20 | export const removeVirtualScheduledFunction = on(VIRTUAL, onVirtualEvent); -------------------------------------------------------------------------------- /src/client/client.interface.ts: -------------------------------------------------------------------------------- 1 | // Run build again or pre-build and all the data types will be available from this import 2 | import { 3 | Partner, 4 | PartnerData, 5 | SystemLog, 6 | } from "./interface.readonly"; 7 | 8 | // Client start 9 | export interface ClientOptions { 10 | partnerId?: string; 11 | accessToken?: string; 12 | version?: number; 13 | prefix?: string; 14 | url?: string | URL; 15 | } 16 | 17 | export interface Client { 18 | addPartner(partner: PartnerData): Promise; 19 | listPartners(): Promise; 20 | listSystemLogs(): Promise; 21 | background(query: Record | URLSearchParams): Promise; 22 | } -------------------------------------------------------------------------------- /src/data/file/save.ts: -------------------------------------------------------------------------------- 1 | import {isR2, saveToR2} from "./r2"; 2 | import {File, FileData} from "./types"; 3 | import {saveToDisk} from "./disk"; 4 | import {setFile} from "./set-file"; 5 | import {KeyValueStore} from "../storage"; 6 | 7 | export async function save(file: FileData, contents: Buffer | Blob, store?: KeyValueStore): Promise { 8 | const update = await saveTo(); 9 | return setFile({ 10 | ...file, 11 | ...update, 12 | }, store) 13 | 14 | async function saveTo() { 15 | if (isR2()) { 16 | return await saveToR2(file, contents); 17 | } else { 18 | return await saveToDisk(file, contents); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/tests/worker/service-worker/wait.ts: -------------------------------------------------------------------------------- 1 | import {DurableServiceWorkerRegistration} from "../../../worker"; 2 | import {dispatchEvent} from "../../../events"; 3 | 4 | export async function waitForServiceWorker(registration: DurableServiceWorkerRegistration) { 5 | if (registration.active) { 6 | return registration.active; 7 | } 8 | // Send a push event to the worker to ensure it is activated 9 | await dispatchEvent({ 10 | type: "push", 11 | serviceWorkerId: registration.durable.serviceWorkerId, 12 | schedule: { 13 | immediate: true 14 | } 15 | }); 16 | await registration.update(); 17 | return waitForServiceWorker(registration); 18 | } -------------------------------------------------------------------------------- /src/data/file/unlink.ts: -------------------------------------------------------------------------------- 1 | import {File, FileData} from "./types"; 2 | import {isR2, unlinkFromR2} from "./r2"; 3 | import {unlinkFromDisk} from "./disk"; 4 | import {KeyValueStore} from "../storage"; 5 | import {deleteFile} from "./delete-file"; 6 | import {getFile} from "./get-file"; 7 | 8 | export async function unlink(file: string | FileData, givenStore?: KeyValueStore) { 9 | if (typeof file === "string") { 10 | file = await getFile(file); 11 | if (!file) { 12 | return; 13 | } 14 | } 15 | if (isR2()) { 16 | await unlinkFromR2(file); 17 | } else { 18 | await unlinkFromDisk(file); 19 | } 20 | await deleteFile(file, givenStore); 21 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "lib": ["es2018", "esnext", "dom"], 5 | "types": ["jest", "node"], 6 | "esModuleInterop": true, 7 | "target": "esnext", 8 | "noImplicitAny": true, 9 | "downlevelIteration": true, 10 | "moduleResolution": "node", 11 | "declaration": true, 12 | "sourceMap": true, 13 | "outDir": "esnext", 14 | "baseUrl": "./src", 15 | "jsx": "react-jsx", 16 | "allowJs": true, 17 | "paths": {}, 18 | "resolveJsonModule": true, 19 | "typeRoots": ["./node_modules/@types", "src/types"] 20 | }, 21 | "include": ["src/**/*", "src/**/*.json"], 22 | "exclude": ["node_modules/@opennetwork/vdom"] 23 | } 24 | -------------------------------------------------------------------------------- /src/tests/worker/service-worker/configured/products.ts: -------------------------------------------------------------------------------- 1 | import {requestMethod} from "../routes.example"; 2 | 3 | export interface Product { 4 | productId: string; 5 | productName: string; 6 | } 7 | 8 | const products: Product[] = [ 9 | { 10 | productId: "apples", 11 | productName: "Apples" 12 | }, 13 | { 14 | productId: "pears", 15 | productName: "Pears" 16 | }, 17 | { 18 | productId: "limes", 19 | productName: "Limes" 20 | } 21 | ] 22 | 23 | console.log("Inside products service worker!") 24 | 25 | requestMethod.get({ pathname: "/products" }, () => { 26 | console.log("Inside products fetch") 27 | return Response.json(products) 28 | }) -------------------------------------------------------------------------------- /src/react/client/components/icons/credit-card.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from "./types"; 2 | 3 | export function CreditCardIcon(props: IconProps) { 4 | return ( 5 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/data/durable-request/get-durable-request.ts: -------------------------------------------------------------------------------- 1 | import {getDurableRequestStore} from "./store"; 2 | import {DurableEventData} from "../durable-event"; 3 | import {ok} from "../../is"; 4 | 5 | export function getDurableRequest(durableRequestId: string) { 6 | const store = getDurableRequestStore(); 7 | return store.get(durableRequestId); 8 | } 9 | 10 | export function getDurableRequestIdForEvent(event: DurableEventData) { 11 | ok(event.durableEventId, "Expected durableEventId"); 12 | return `${event.type}:request:${event.durableEventId}`; 13 | } 14 | 15 | export function getDurableRequestForEvent(event: DurableEventData) { 16 | return getDurableRequest( 17 | getDurableRequestIdForEvent(event) 18 | ); 19 | } -------------------------------------------------------------------------------- /src/tests/worker/service-worker/store/post.ts: -------------------------------------------------------------------------------- 1 | import {requestMethod} from "../routes.example"; 2 | import {v4} from "uuid"; 3 | import {cache} from "./cache"; 4 | 5 | requestMethod.post({ pathname: "/:type" }, async request => { 6 | const id = v4() 7 | const url = new URL(request.url); 8 | url.pathname = `${url.pathname}/${id}`; 9 | await cache.put(url, new Response(request.body, { 10 | headers: { 11 | "Content-Type": request.headers.get("Content-Type"), 12 | "Last-Modified": new Date().toUTCString() 13 | } 14 | })); 15 | return new Response(null, { 16 | status: 201, 17 | headers: { 18 | Location: url.toString() 19 | } 20 | }); 21 | }) -------------------------------------------------------------------------------- /src/data/file/set-file.ts: -------------------------------------------------------------------------------- 1 | import { resolveFileStore } from "./store"; 2 | import { File, FileData } from "./types"; 3 | import { v4 } from "uuid"; 4 | import {KeyValueStore} from "../storage"; 5 | 6 | export async function setFile( 7 | data: FileData & Partial, 8 | givenStore?: KeyValueStore 9 | ): Promise { 10 | const fileId = data.fileId || v4(); 11 | const createdAt = data.createdAt || new Date().toISOString(); 12 | const meta: File = { 13 | updatedAt: createdAt, 14 | createdAt, 15 | ...data, 16 | uploadedAt: data.uploadedAt || createdAt, 17 | fileId, 18 | }; 19 | const store = resolveFileStore(meta, givenStore); 20 | await store.set(fileId, meta); 21 | return meta; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/listen/task/index.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance } from "fastify"; 2 | import { listTaskRoutes } from "./list-tasks"; 3 | import { addTaskRoutes } from "./add-task"; 4 | import { getTaskRoutes } from "./get-task"; 5 | import { setTaskRoutes } from "./set-task"; 6 | import { patchTaskRoutes } from "./patch-task"; 7 | 8 | export async function taskRoutes(fastify: FastifyInstance) { 9 | async function routes(fastify: FastifyInstance) { 10 | fastify.register(listTaskRoutes); 11 | fastify.register(addTaskRoutes); 12 | fastify.register(getTaskRoutes); 13 | fastify.register(setTaskRoutes); 14 | fastify.register(patchTaskRoutes); 15 | } 16 | 17 | fastify.register(routes, { 18 | prefix: "/tasks", 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/react/client/components/icons/user-circle.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from "./types"; 2 | 3 | export function UserCircleIcon(props: IconProps) { 4 | return ( 5 | 14 | 15 | 16 | ); 17 | } -------------------------------------------------------------------------------- /src/react/client/components/happening/context.ts: -------------------------------------------------------------------------------- 1 | import {HappeningTree} from "../../../../client"; 2 | import {createContext, useContext} from "react"; 3 | import {ok} from "../../../../is"; 4 | 5 | export const TimezoneContext = createContext("Pacific/Auckland"); 6 | export const TimezoneProvider = TimezoneContext.Provider; 7 | 8 | export const HappeningContext = createContext(undefined); 9 | export const HappeningProvider = HappeningContext.Provider; 10 | 11 | export function useHappening(): HappeningTree { 12 | const context = useContext(HappeningContext); 13 | ok(context); 14 | return context; 15 | } 16 | 17 | export function useTimezone(): string { 18 | return useContext(TimezoneContext); 19 | } -------------------------------------------------------------------------------- /src/react/client/components/happening/happening.tsx: -------------------------------------------------------------------------------- 1 | import { HappeningTree } from "../../../../client"; 2 | import {HappeningAt} from "./at"; 3 | import { HappeningProvider} from "./context"; 4 | 5 | export interface HappeningProps { 6 | happening: HappeningTree 7 | } 8 | 9 | export function Happening(props: HappeningProps) { 10 | const { happening } = props; 11 | return ( 12 | 13 |
14 |
{happening.title}
15 |
{happening.type}
16 | 17 |
{happening.children?.length || 0} children
18 |
19 |
20 | ) 21 | } -------------------------------------------------------------------------------- /src/data/authorisation/types.ts: -------------------------------------------------------------------------------- 1 | import {AuthorisationNotificationData} from "../authorisation-notification"; 2 | 3 | export type AttendeeAuthorisationType = "attendee"; 4 | export type HappeningAuthorisationType = "happening"; 5 | 6 | export type AuthorisationType = AttendeeAuthorisationType | HappeningAuthorisationType; 7 | 8 | export interface AuthorisationData { 9 | type: AuthorisationType; 10 | attendeeId?: string; 11 | happeningId?: string; 12 | notifications?: AuthorisationNotificationData[]; 13 | } 14 | 15 | export interface Authorisation extends AuthorisationData { 16 | authorisationId: string; 17 | createdAt: string; 18 | updatedAt: string; 19 | notifiedAt?: string; 20 | declinedAt?: string; 21 | authorisedAt?: string; 22 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import "./references"; 2 | import "./scheduled"; 3 | import "./dispatch"; 4 | 5 | // export * from "./data"; 6 | // export * from "./config"; 7 | // export * from "./listen"; 8 | // export * from "./view"; 9 | // export * from "./package"; 10 | // export * from "./react/server"; 11 | // export * as authentication from "./authentication"; 12 | // export * from "./remote"; 13 | // export * from "./is"; 14 | export * from "./email"; 15 | export * from "./events"; 16 | // export * from "./branding"; 17 | export * from "./fetch"; 18 | export * from "./sync"; 19 | export * from "./content-index"; 20 | export * from "./periodic-sync"; 21 | export * from "./worker"; 22 | // export * from "./start"; 23 | export * from "./storage-buckets"; 24 | export * from "./scheduler"; -------------------------------------------------------------------------------- /src/react/client/components/icons/eye-dropper.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from "./types"; 2 | 3 | export function EyeDropperIcon(props: IconProps) { 4 | return ( 5 | 14 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/data/data.ts: -------------------------------------------------------------------------------- 1 | export * from "./partner"; 2 | export * from "./system-log"; 3 | export * from "./access-token"; 4 | export * from "./background"; 5 | export * from "./attendee"; 6 | export * from "./authentication-state"; 7 | export * from "./happening"; 8 | export * from "./authorisation"; 9 | export * from "./authorisation-notification"; 10 | export * from "./storage"; 11 | export * from "./user"; 12 | export * from "./authentication-role"; 13 | export * from "./organisation"; 14 | export * from "./form-meta"; 15 | export * from "./cache"; 16 | export * from "./identifier"; 17 | export * from "./file"; 18 | export * from "./user-credential"; 19 | export * from "./task"; 20 | export * from "./change"; 21 | export * from "./durable-event"; 22 | export * from "./durable-request"; -------------------------------------------------------------------------------- /src/listen/branding/logo.ts: -------------------------------------------------------------------------------- 1 | import {FastifyInstance} from "fastify"; 2 | import {getBrandingLogo} from "../../branding"; 3 | 4 | export async function logoRoutes(fastify: FastifyInstance) { 5 | try { 6 | fastify.get("/logo", { 7 | async handler(request, response) { 8 | const { buffer, contentType, url } = await getBrandingLogo(); 9 | if (!buffer) { 10 | response.header("Location", url); 11 | response.status(302); 12 | response.send(); 13 | } else { 14 | response.header("Content-Type", contentType); 15 | response.send(buffer); 16 | } 17 | } 18 | }) 19 | } catch {} 20 | 21 | } -------------------------------------------------------------------------------- /src/tests/worker/service-worker/loop/a.ts: -------------------------------------------------------------------------------- 1 | import {requestMethod} from "../routes.example"; 2 | import {getRegistrations} from "./registration"; 3 | 4 | const { b } = await getRegistrations(); 5 | 6 | requestMethod.post("/", async (request) => { 7 | const { limit, count, visitedA, ...rest } = await request.json(); 8 | 9 | console.log("A", { limit, count, visitedA, ...rest }); 10 | 11 | if (count && count >= limit) { 12 | return Response.json({ count, final: "a", visitedA, ...rest }); 13 | } 14 | 15 | return b.fetch("/", { 16 | method: "POST", 17 | body: JSON.stringify({ 18 | limit, 19 | count: (count ?? 0) + 1, 20 | ...rest, 21 | visitedA: (visitedA ?? 0) + 1 22 | }) 23 | }) 24 | }); -------------------------------------------------------------------------------- /src/tests/worker/service-worker/loop/b.ts: -------------------------------------------------------------------------------- 1 | import {requestMethod} from "../routes.example"; 2 | import {getRegistrations} from "./registration"; 3 | 4 | const { c } = await getRegistrations(); 5 | 6 | requestMethod.post("/", async (request) => { 7 | const { limit, count, visitedB, ...rest } = await request.json(); 8 | 9 | console.log("B", { limit, count, visitedB, ...rest }); 10 | 11 | if (count && count >= limit) { 12 | return Response.json({ count, final: "b", visitedB, ...rest }); 13 | } 14 | 15 | return c.fetch("/", { 16 | method: "POST", 17 | body: JSON.stringify({ 18 | limit, 19 | count: (count ?? 0) + 1, 20 | ...rest, 21 | visitedB: (visitedB ?? 0) + 1 22 | }) 23 | }) 24 | }); -------------------------------------------------------------------------------- /src/tests/worker/service-worker/loop/c.ts: -------------------------------------------------------------------------------- 1 | import {requestMethod} from "../routes.example"; 2 | import {getRegistrations} from "./registration"; 3 | 4 | const { a } = await getRegistrations(); 5 | 6 | requestMethod.post("/", async (request) => { 7 | const { limit, count, visitedC, ...rest } = await request.json(); 8 | 9 | console.log("C", { limit, count, visitedC, ...rest }); 10 | 11 | if (count && count >= limit) { 12 | return Response.json({ count, final: "c", visitedC, ...rest }); 13 | } 14 | 15 | return a.fetch("/", { 16 | method: "POST", 17 | body: JSON.stringify({ 18 | limit, 19 | count: (count ?? 0) + 1, 20 | ...rest, 21 | visitedC: (visitedC ?? 0) + 1 22 | }) 23 | }) 24 | }); -------------------------------------------------------------------------------- /src/data/authentication-role/get-role.ts: -------------------------------------------------------------------------------- 1 | import {getUserAuthenticationRoleStore} from "./store"; 2 | import {User} from "../user"; 3 | import {setUserAuthenticationRole} from "./set-role"; 4 | 5 | export async function getUserAuthenticationRole(userId: string) { 6 | const store = getUserAuthenticationRoleStore(); 7 | return await store.get(userId); 8 | } 9 | 10 | export async function getUserAuthenticationRoleForUser(user: User) { 11 | const role = await getUserAuthenticationRole(user.userId); 12 | if (!role) return role; 13 | // Update the expiry on access to allow retention 14 | // 15 | // No access, no user, data expires 16 | await setUserAuthenticationRole({ 17 | ...role, 18 | expiresAt: user.expiresAt 19 | }); 20 | return role; 21 | } -------------------------------------------------------------------------------- /src/storage-buckets/types.ts: -------------------------------------------------------------------------------- 1 | export interface StorageBucketOptions { 2 | persisted?: boolean; 3 | quota?: number; 4 | expires?: number; 5 | } 6 | 7 | export interface StorageBucketManager { 8 | open(name: string, options?: StorageBucketOptions): Promise 9 | keys(): Promise; 10 | delete(name: string): Promise; 11 | } 12 | 13 | export interface StorageBucket { 14 | name: string; 15 | 16 | persist(): Promise; 17 | persisted(): Promise; 18 | estimate(): Promise; 19 | setExpires(expires: number): Promise; 20 | expires(): Promise; 21 | indexedDb?: unknown; 22 | caches: CacheStorage; 23 | 24 | getDirectory(): Promise; 25 | } -------------------------------------------------------------------------------- /src/listen/change/index.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance } from "fastify"; 2 | import { listChangeRoutes } from "./list-changes"; 3 | import { addChangeRoutes } from "./add-change"; 4 | import { getChangeRoutes } from "./get-change"; 5 | import { setChangeRoutes } from "./set-change"; 6 | import { patchChangeRoutes } from "./patch-change"; 7 | 8 | export async function changeRoutes(fastify: FastifyInstance) { 9 | async function routes(fastify: FastifyInstance) { 10 | fastify.register(listChangeRoutes); 11 | fastify.register(addChangeRoutes); 12 | fastify.register(getChangeRoutes); 13 | fastify.register(setChangeRoutes); 14 | fastify.register(patchChangeRoutes); 15 | } 16 | 17 | fastify.register(routes, { 18 | prefix: "/changes/:changeType/:targetType", 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/worker/service-worker/dispatchers/install.ts: -------------------------------------------------------------------------------- 1 | import {dispatcher} from "../../../events/schedule/schedule"; 2 | import {createWaitUntil, ExtendableEvent} from "../../../fetch"; 3 | import {addRoutes, AddRoutesOptions} from "../router"; 4 | import {skipWaiting} from "../skip-waiting"; 5 | 6 | export interface InstallEvent extends ExtendableEvent { 7 | skipWaiting(): void; 8 | addRoutes(rules: AddRoutesOptions): Promise | unknown; 9 | } 10 | 11 | export const removeInstallDispatcher = dispatcher("install", async (event, dispatch) => { 12 | const { 13 | wait, 14 | waitUntil 15 | } = createWaitUntil(event); 16 | await dispatch({ 17 | ...event, 18 | waitUntil, 19 | skipWaiting, 20 | addRoutes 21 | }) 22 | await wait(); 23 | }); -------------------------------------------------------------------------------- /src/data/organisation/set-organisation.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from "uuid"; 2 | import { Organisation, PartialOrganisation } from "./types"; 3 | import { getOrganisationStore } from "./store"; 4 | 5 | export async function setOrganisation( 6 | data: PartialOrganisation 7 | ): Promise { 8 | const store = getOrganisationStore(); 9 | const organisationId = data.organisationId || v4(); 10 | const updatedAt = new Date().toISOString(); 11 | const createdAt = data.createdAt || updatedAt; 12 | const organisation: Organisation = { 13 | approved: false, 14 | approvedAt: undefined, 15 | approvedByUserId: undefined, 16 | ...data, 17 | organisationId, 18 | createdAt, 19 | updatedAt, 20 | }; 21 | await store.set(organisationId, organisation); 22 | return organisation; 23 | } 24 | -------------------------------------------------------------------------------- /src/react/client/components/icons/bag.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from "./types"; 2 | 3 | export function ShoppingBagIcon(props: IconProps) { 4 | return ( 5 | 14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/types/worker-configuration.d.ts: -------------------------------------------------------------------------------- 1 | interface Env { 2 | // Example binding to KV. Learn more at https://developers.cloudflare.com/workers/runtime-apis/kv/ 3 | // MY_KV_NAMESPACE: KVNamespace; 4 | // 5 | // Example binding to Durable Object. Learn more at https://developers.cloudflare.com/workers/runtime-apis/durable-objects/ 6 | // MY_DURABLE_OBJECT: DurableObjectNamespace; 7 | // 8 | // Example binding to R2. Learn more at https://developers.cloudflare.com/workers/runtime-apis/r2/ 9 | // MY_BUCKET: R2Bucket; 10 | // 11 | // Example binding to a Service. Learn more at https://developers.cloudflare.com/workers/runtime-apis/service-bindings/ 12 | // MY_SERVICE: Fetcher; 13 | // 14 | // Example binding to a Queue. Learn more at https://developers.cloudflare.com/queues/javascript-apis/ 15 | // MY_QUEUE: Queue; 16 | } 17 | -------------------------------------------------------------------------------- /src/client/interface.readonly.ts: -------------------------------------------------------------------------------- 1 | // These references are auto generated, do not edit manually 2 | 3 | export * from "../data/attendee/types" 4 | export * from "../data/authentication-role/types" 5 | export * from "../data/authorisation/types" 6 | export * from "../data/authorisation-notification/types" 7 | export * from "../data/change/types" 8 | export * from "../data/durable-event/types" 9 | export * from "../data/durable-request/types" 10 | export * from "../data/expiring/types" 11 | export * from "../data/file/types" 12 | export * from "../data/form-meta/types" 13 | export * from "../data/happening/types" 14 | export * from "../data/identifier/types" 15 | export * from "../data/organisation/types" 16 | export * from "../data/partner/types" 17 | export * from "../data/system-log/types" 18 | export * from "../data/task/types" 19 | -------------------------------------------------------------------------------- /src/data/form-meta/set-form-meta.ts: -------------------------------------------------------------------------------- 1 | import { getFormMetaStore } from "./store"; 2 | import { FormMeta, FormMetaData } from "./types"; 3 | import { v4 } from "uuid"; 4 | import {getMaybePartner, getMaybeUser} from "../../authentication"; 5 | 6 | export async function setFormMeta( 7 | data: FormMetaData & Partial 8 | ): Promise { 9 | const store = getFormMetaStore(); 10 | const formMetaId = data.formMetaId || v4(); 11 | const createdAt = data.createdAt || new Date().toISOString(); 12 | const userId = getMaybeUser()?.userId; 13 | const partnerId = getMaybePartner()?.partnerId; 14 | const meta: FormMeta = { 15 | updatedAt: createdAt, 16 | createdAt, 17 | ...data, 18 | partnerId, 19 | userId, 20 | formMetaId, 21 | }; 22 | await store.set(formMetaId, meta); 23 | return meta; 24 | } 25 | -------------------------------------------------------------------------------- /src/react/client/components/happening/at.tsx: -------------------------------------------------------------------------------- 1 | import {HappeningEventData} from "../../../../client"; 2 | import {DateTime} from "luxon"; 3 | import {useHappening, useTimezone} from "./context"; 4 | 5 | 6 | export function HappeningAt() { 7 | const happening = useHappening(); 8 | const timezone = useTimezone(); 9 | let message = "This event is happening"; 10 | if (happening.startedAt) { 11 | message = `Started at ${happening.startAt}` 12 | } else if (happening.startAt) { 13 | message = `Starts at ${happening.startAt}` 14 | } else if (happening.endedAt) { 15 | message = `Ended at ${happening.endedAt}` 16 | } else if (happening.endAt) { 17 | message = `Ends at ${happening.endAt}` 18 | } 19 | return ( 20 |
21 | {message} 22 |
23 | ) 24 | } -------------------------------------------------------------------------------- /src/data/change/list-changes.ts: -------------------------------------------------------------------------------- 1 | import {Change, ChangeTarget, ChangeTargetIdentifier, ChangeTargetType} from "./types"; 2 | import { getChangeStore } from "./store"; 3 | import {isLike} from "../../is"; 4 | 5 | export interface ListChangesInput extends ChangeTargetIdentifier { 6 | target: ChangeTargetType | ChangeTarget; 7 | } 8 | 9 | export async function listChanges(options: ListChangesInput): Promise { 10 | const store = getChangeStore(options); 11 | let changes = await store.values(); 12 | if (isChangeTarget(options.target)) { 13 | const id = options.target.id; 14 | changes = changes.filter(change => change.target.id === id); 15 | } 16 | return changes; 17 | } 18 | 19 | function isChangeTarget(target: ChangeTargetType | ChangeTarget): target is ChangeTarget { 20 | return isLike(target) && !!target.id; 21 | } -------------------------------------------------------------------------------- /src/data/authentication-state/store.ts: -------------------------------------------------------------------------------- 1 | import { DAY_MS, getExpiringStore, MINUTE_MS } from "../expiring-kv"; 2 | import { AuthenticationState } from "./types"; 3 | 4 | const STORE_NAME = "authenticationState"; 5 | 6 | export const DEFAULT_AUTHENTICATION_STATE_EXPIRES_MS = 5 * MINUTE_MS; 7 | export const DEFAULT_COOKIE_STATE_EXPIRES_MS = 14 * DAY_MS; 8 | // Magic email link lasts 10 minutes 9 | export const DEFAULT_AUTHSIGNAL_STATE_EXPIRES_MS = 12 * MINUTE_MS; 10 | 11 | export const DEFAULT_INVITEE_STATE_EXPIRES_MS = 3 * DAY_MS; 12 | export const DEFAULT_INVITEE_EXCHANGE_STATE_EXPIRES_MS = 3 * DEFAULT_AUTHENTICATION_STATE_EXPIRES_MS; 13 | 14 | export const EXTERNAL_STATE_ID_SEPARATOR = "::"; 15 | 16 | export function getAuthenticationStateStore() { 17 | return getExpiringStore(STORE_NAME, { 18 | counter: false, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/data/durable-event/store.ts: -------------------------------------------------------------------------------- 1 | import {getHappeningStore} from "../happening"; 2 | import {DurableEvent, DurableEventTypeData} from "./types"; 3 | import {listKeyValueStoreIndex} from "../storage/store-index"; 4 | import {isMetaStoreName} from "../storage/kv-base"; 5 | 6 | const STORE_PREFIX = "event:"; 7 | 8 | 9 | export function getDurableEventStore({ type }: DurableEventTypeData) { 10 | return getHappeningStore(`${STORE_PREFIX}${type}`, { 11 | counter: false 12 | }); 13 | } 14 | 15 | /** 16 | * Note that this is only usable if KEY_VALUE_STORE_INDEX is enabled 17 | */ 18 | export async function listDurableEventTypes() { 19 | const stores = await listKeyValueStoreIndex(); 20 | return stores 21 | .filter(name => name.startsWith(STORE_PREFIX)) 22 | .map(name => name.substring(STORE_PREFIX.length)); 23 | } -------------------------------------------------------------------------------- /src/data/task/schema.ts: -------------------------------------------------------------------------------- 1 | import {identifierSchema} from "../identifier"; 2 | import {happeningSchema} from "../happening"; 3 | 4 | export const taskData = { 5 | type: "object", 6 | properties: { 7 | ...happeningSchema.happeningData.properties, 8 | title: { 9 | type: "string", 10 | }, 11 | identifiers: { 12 | type: "array", 13 | items: identifierSchema.identifier 14 | } 15 | }, 16 | required: ["identifiers", "title"], 17 | } as const; 18 | 19 | export const task = { 20 | type: "object", 21 | properties: { 22 | taskId: { 23 | type: "string", 24 | }, 25 | createdAt: { 26 | type: "string", 27 | }, 28 | updatedAt: { 29 | type: "string", 30 | }, 31 | ...taskData.properties, 32 | }, 33 | required: ["taskId", "createdAt", "updatedAt", ...taskData.required], 34 | } as const; 35 | -------------------------------------------------------------------------------- /src/data/happening/set-happening.ts: -------------------------------------------------------------------------------- 1 | import {Happening, PartialHappening} from "./types"; 2 | import {v4} from "uuid"; 3 | import {getHappeningStore} from "./store"; 4 | import {getMaybePartner, getMaybeUser} from "../../authentication"; 5 | 6 | export async function setHappening(data: PartialHappening) { 7 | const createdAt = data.createdAt || new Date().toISOString(); 8 | const happeningId = data.happeningId || v4(); 9 | const type = data.type || "event"; 10 | const partnerId = getMaybePartner()?.partnerId; 11 | const userId = getMaybeUser()?.userId; 12 | const happening: Happening = { 13 | ...data, 14 | type, 15 | createdAt, 16 | happeningId, 17 | partnerId, 18 | userId 19 | }; 20 | const store = getHappeningStore(); 21 | await store.set(happeningId, happening); 22 | return happening; 23 | } -------------------------------------------------------------------------------- /src/react/client/components/icons/annotation.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from "./types"; 2 | 3 | export function AnnotationIcon(props: IconProps) { 4 | return ( 5 | 14 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/view/views.ts: -------------------------------------------------------------------------------- 1 | import {getConfig, setConfig} from "../config"; 2 | import {views as defaultViews} from "../react/server/paths"; 3 | import {View} from "./types"; 4 | 5 | export function getViews(): View[] { 6 | const config = getConfig(); 7 | const { views = [] } = config; 8 | 9 | const includedPaths = views.map(view => view.path); 10 | const baseViews = defaultViews 11 | .filter(view => !includedPaths.includes(view.path)); 12 | 13 | if (!baseViews.length) return views; 14 | 15 | const allViews = [...baseViews, ...views]; 16 | 17 | // Next time getConfig is called, no base views will be included 18 | setConfig({ ...config, views: allViews }); 19 | 20 | return [...baseViews, ...views]; 21 | } 22 | 23 | export function getView(path: string) { 24 | const views = getViews(); 25 | return views.find(view => view.path === path); 26 | } -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | import {name, root, version} from "../package"; 2 | import {createContext} from "../hooks/context"; 3 | import {Config} from "./types"; 4 | 5 | export * from "./env"; 6 | 7 | export { Config } from "./types"; 8 | 9 | const ConfigContext = createContext({ 10 | name, 11 | version, 12 | root 13 | }); 14 | 15 | export function getConfig(overrides?: Partial): Config { 16 | if (!overrides) return ConfigContext.value; 17 | return { 18 | ...ConfigContext.value, 19 | ...overrides 20 | }; 21 | } 22 | 23 | export function withConfig(config: Partial, fn: () => R): R { 24 | return ConfigContext.run({ ...getConfig(), ...config }, fn); 25 | } 26 | 27 | export function setConfig(config: Config) { 28 | for (const [key, value] of Object.entries(config)) { 29 | ConfigContext.set(key, value); 30 | } 31 | } -------------------------------------------------------------------------------- /src/data/organisation/types.ts: -------------------------------------------------------------------------------- 1 | export interface OrganisationBaseData extends Record { 2 | countryCode?: string; // "NZ" 3 | location?: string; 4 | remote?: boolean; 5 | onsite?: boolean; 6 | pharmacy?: boolean; 7 | delivery?: boolean; 8 | clinic?: boolean; 9 | website?: string; 10 | associatedBrandingTerms?: string[]; // Eg common names used to refer to the organisation by way of brand 11 | } 12 | 13 | export interface OrganisationData extends OrganisationBaseData { 14 | organisationName: string; 15 | partnerId?: string; 16 | approved?: boolean; 17 | approvedAt?: string; 18 | } 19 | 20 | export interface Organisation extends OrganisationData { 21 | organisationId: string; 22 | createdAt: string; 23 | updatedAt: string; 24 | approvedByUserId?: string; 25 | } 26 | 27 | export type PartialOrganisation = OrganisationData & Partial; -------------------------------------------------------------------------------- /src/data/happening/delete-happening.ts: -------------------------------------------------------------------------------- 1 | import {getHappeningStore} from "./store"; 2 | import {getTopHappeningTree} from "./get-happening-tree"; 3 | import {HappeningTree} from "./types"; 4 | import {ok} from "../../is"; 5 | 6 | export async function deleteHappening(happeningId: string) { 7 | const store = getHappeningStore(); 8 | return store.delete(happeningId); 9 | } 10 | 11 | export async function deleteHappeningTree(happeningId: string) { 12 | const tree = await getTopHappeningTree(happeningId); 13 | const identifiers = [...new Set(getIdentifiers(tree))]; 14 | ok(identifiers.includes(happeningId)); 15 | await Promise.all( 16 | identifiers.map(deleteHappening) 17 | ); 18 | 19 | function getIdentifiers(tree: HappeningTree): string[] { 20 | return [ 21 | tree.id, 22 | ...tree.children.flatMap(getIdentifiers) 23 | ]; 24 | } 25 | } -------------------------------------------------------------------------------- /src/data/user/types.ts: -------------------------------------------------------------------------------- 1 | import { Expiring } from "../expiring"; 2 | import { AuthenticationStateType } from "../authentication-state"; 3 | 4 | 5 | export interface ExternalUserType { 6 | externalType: AuthenticationStateType; 7 | } 8 | 9 | export interface UserData extends Expiring, Partial, Record { 10 | 11 | } 12 | 13 | export interface ExternalUserReferenceData extends ExternalUserType { 14 | externalId: string; 15 | externalType: AuthenticationStateType; 16 | } 17 | 18 | export interface UserIdHistoryItem { 19 | userId: string; 20 | replacedAt: string; 21 | } 22 | 23 | export interface ExternalUserReference extends Expiring, ExternalUserType { 24 | userId: string; 25 | userIdHistory?: UserIdHistoryItem[] 26 | } 27 | 28 | export interface User extends UserData { 29 | userId: string; 30 | createdAt: string; 31 | updatedAt: string; 32 | } 33 | -------------------------------------------------------------------------------- /public/watermark.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | 17 | virtualstate.dev 18 | virtualstate.dev 19 | virtualstate.dev 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/types/tree-sitter.d.ts: -------------------------------------------------------------------------------- 1 | // declare module "tree-sitter"; 2 | declare module "tree-sitter-capnp"; 3 | 4 | // declare module "tree-sitter" { 5 | // 6 | // export interface Language { 7 | // nodeTypeInfo: unknown 8 | // } 9 | // 10 | // export interface Node { 11 | // toString(): string; 12 | // child(index: number): Node; 13 | // firstChild: Node; 14 | // lastChild: Node; 15 | // 16 | // } 17 | // 18 | // export interface Tree { 19 | // rootNode: Node 20 | // } 21 | // 22 | // export default class Parser { 23 | // setLanguage(language: Language): void; 24 | // parse(source: string): Tree 25 | // } 26 | // 27 | // } 28 | // 29 | // declare module "tree-sitter-capnp" { 30 | // 31 | // import { Language } from "tree-sitter"; 32 | // 33 | // const Capnp: Language 34 | // 35 | // export default Capnp; 36 | // } -------------------------------------------------------------------------------- /src/data/durable-event/add-durable-event.ts: -------------------------------------------------------------------------------- 1 | import {v4} from "uuid"; 2 | import {DurableEvent, DurableEventData} from "./types"; 3 | import {getDurableEventStore} from "./store"; 4 | import {ok} from "../../is"; 5 | 6 | export async function addDurableEvent(event: DurableEventData) { 7 | return setDurableEvent(event); 8 | } 9 | 10 | export async function setDurableEvent(event: DurableEventData) { 11 | const createdAt = event.createdAt || new Date().toISOString(); 12 | const eventId = event.durableEventId || v4(); 13 | const durable: DurableEvent = { 14 | timeStamp: Date.now(), 15 | createdAt, 16 | updatedAt: createdAt, 17 | durableEventId: eventId, 18 | ...event 19 | }; 20 | ok(!durable.virtual, "Cannot store virtual event"); 21 | const store = getDurableEventStore(durable); 22 | await store.set(eventId, durable); 23 | return durable; 24 | } -------------------------------------------------------------------------------- /src/react/client/components/icons/trash.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from "./types"; 2 | 3 | export function TrashIcon(props: IconProps) { 4 | return ( 5 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/tests/worker/service-worker/loop/registration.ts: -------------------------------------------------------------------------------- 1 | import {fileURLToPath} from "node:url"; 2 | import {dirname, join} from "node:path"; 3 | import {serviceWorker} from "../../../../worker"; 4 | 5 | const pathname = fileURLToPath(import.meta.url); 6 | 7 | const pathA = join(dirname(pathname), "./a.js"); 8 | const pathB = join(dirname(pathname), "./b.js"); 9 | const pathC = join(dirname(pathname), "./c.js"); 10 | 11 | export async function register() { 12 | const a = await serviceWorker.register(pathA); 13 | const b = await serviceWorker.register(pathB); 14 | const c = await serviceWorker.register(pathC); 15 | return { a, b, c } 16 | } 17 | 18 | export async function getRegistrations() { 19 | const a = await serviceWorker.getRegistration(pathA); 20 | const b = await serviceWorker.getRegistration(pathB); 21 | const c = await serviceWorker.getRegistration(pathC); 22 | return { a, b, c } 23 | } -------------------------------------------------------------------------------- /src/listen/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance } from "fastify"; 2 | import { discordAuthenticationRoutes } from "./discord"; 3 | import { redditAuthenticationRoutes } from "./reddit"; 4 | import { authsignalAuthenticationRoutes } from "./authsignal"; 5 | import { logoutRoutes } from "./logout"; 6 | import { webauthnRoutes } from "./webauthn"; 7 | import { anonymousRoutes } from "./anonymous"; 8 | 9 | export async function authenticationRoutes(fastify: FastifyInstance) { 10 | async function routes(fastify: FastifyInstance) { 11 | fastify.register(discordAuthenticationRoutes); 12 | fastify.register(redditAuthenticationRoutes); 13 | fastify.register(authsignalAuthenticationRoutes); 14 | fastify.register(webauthnRoutes); 15 | fastify.register(logoutRoutes); 16 | fastify.register(anonymousRoutes); 17 | } 18 | 19 | fastify.register(routes, { 20 | prefix: "/authentication", 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/tests/readme/content-index/index.js: -------------------------------------------------------------------------------- 1 | import { index, caches } from "@virtualstate/internal"; 2 | 3 | const entry = { 4 | id: "post-1", 5 | url: "/posts/amet.html", 6 | title: "Amet consectetur adipisicing", 7 | description: 8 | "Repellat et quia iste possimus ducimus aliquid a aut eaque nostrum.", 9 | icons: [ 10 | { 11 | src: "https://javascript.org.nz/logo.png", 12 | sizes: "200x200", 13 | type: "image/png", 14 | }, 15 | ], 16 | category: "article", 17 | }; 18 | await index.add(entry); 19 | 20 | console.log(await index.getAll()) // [{ id: "post-1" }] 21 | 22 | const cache = await caches.open("contentIndex"); 23 | 24 | for (const { src } of entry.icons) { 25 | const response = await cache.match(src); 26 | const { byteLength } = await response.arrayBuffer(); 27 | console.log(src, response.status, byteLength) // ... 200 5348 28 | } -------------------------------------------------------------------------------- /src/start.ts: -------------------------------------------------------------------------------- 1 | import {Config, getConfig, withConfig} from "./config"; 2 | import {isServiceWorker} from "./worker/service-worker/config"; 3 | 4 | export async function start(config?: Partial): Promise<() => Promise> { 5 | if (config) { 6 | return withConfig(getConfig(config), () => start()); 7 | } 8 | 9 | await import("./scheduled"); 10 | await import("./dispatch"); 11 | 12 | const tracing = await import("./tracing"); 13 | let listen: { close(): Promise } | undefined, 14 | worker: { close(): Promise } | undefined; 15 | 16 | if (isServiceWorker()) { 17 | worker = await import("./worker/service-worker/main"); 18 | } else { 19 | listen = await import("./listen/main"); 20 | } 21 | 22 | return async function close() { 23 | await tracing.shutdown(); 24 | await listen?.close(); 25 | await worker?.close(); 26 | } 27 | } -------------------------------------------------------------------------------- /src/view/types.ts: -------------------------------------------------------------------------------- 1 | import {FastifyPluginAsync, FastifyReply, FastifyRequest} from "fastify"; 2 | import {FunctionComponent} from "react"; 3 | import {OpenNetworkServerProps} from "../react/server"; 4 | 5 | type UnknownResult = void | unknown | Promise 6 | export interface HandlerFn { 7 | (request: FastifyRequest, response: FastifyReply, data?: unknown): UnknownResult; 8 | } 9 | 10 | export interface View { 11 | path: string; 12 | anonymous?: boolean; 13 | cached?: boolean; 14 | deferHandlerWhenSubmit?: boolean; 15 | handler?: HandlerFn; 16 | submit?: HandlerFn; 17 | Component: FunctionComponent; 18 | } 19 | 20 | export interface PartialView extends Partial { 21 | path: string; 22 | } 23 | 24 | export interface ViewConfig { 25 | views?: View[]; 26 | Component?: FunctionComponent 27 | routes?: FastifyPluginAsync 28 | handler?: HandlerFn; 29 | } -------------------------------------------------------------------------------- /src/react/client/components/icons/wrench.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from "./types"; 2 | 3 | export function WrenchIcon(props: IconProps) { 4 | return ( 5 | 14 | 19 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/data/task/list-task-trees.ts: -------------------------------------------------------------------------------- 1 | import {listHappenings, listHappeningTrees, ListHappeningTreesInput, listHappeningTreesWithContext} from "../happening"; 2 | import {Task} from "./types"; 3 | import {createTaskHappeningTreeContext} from "./create-task-happening-context"; 4 | import {listTasks, ListTasksInput} from "./list-tasks"; 5 | import {getAttendee} from "../attendee"; 6 | 7 | export interface ListTaskTreesOptions extends ListHappeningTreesInput, ListTasksInput { 8 | 9 | } 10 | 11 | export async function listTaskTrees(options: ListTaskTreesOptions) { 12 | const tasks = await listTasks(options); 13 | const attendeeIds = [...new Set(tasks.flatMap(task => task.attendees))]; 14 | const attendees = await Promise.all(attendeeIds.map(getAttendee)); 15 | 16 | const context = createTaskHappeningTreeContext({ 17 | happenings: tasks, 18 | attendees 19 | }) 20 | return listHappeningTreesWithContext(context); 21 | } -------------------------------------------------------------------------------- /src/tests/worker/service-worker/chain/index.ts: -------------------------------------------------------------------------------- 1 | import {serviceWorker} from "../../../../worker"; 2 | import {fileURLToPath} from "node:url"; 3 | import {dirname, join} from "node:path"; 4 | 5 | const pathname = fileURLToPath(import.meta.url); 6 | 7 | const pathA = join(dirname(pathname), "./a.js"); 8 | const pathB = join(dirname(pathname), "./b.js"); 9 | const pathC = join(dirname(pathname), "./c.js"); 10 | 11 | const a = await serviceWorker.register(pathA); 12 | const b = await serviceWorker.register(pathB); 13 | const c = await serviceWorker.register(pathC); 14 | 15 | const responseA = await a.fetch("/", { 16 | method: "POST", 17 | body: "Initial" 18 | }); 19 | const responseB = await b.fetch("/", { 20 | method: "POST", 21 | body: responseA.body 22 | }); 23 | const responseC = await c.fetch("/", { 24 | method: "POST", 25 | body: responseB.body 26 | }); 27 | const finalText = await responseC.text(); 28 | console.log({ finalText }); 29 | 30 | -------------------------------------------------------------------------------- /scripts/replace-between.js: -------------------------------------------------------------------------------- 1 | import {promises as fs} from "node:fs"; 2 | 3 | export async function replaceBetween(fileName, tagName, content) { 4 | const tag = `[//]: # (${tagName})`; 5 | 6 | const readMe = await fs.readFile(fileName, "utf8"); 7 | const badgeStart = readMe.indexOf(tag); 8 | const badgeStartAfter = badgeStart + tag.length; 9 | if (badgeStart === -1) { 10 | console.warn(`Expected to find "${tag}" in ${fileName}`); 11 | return; 12 | } 13 | const badgeEnd = badgeStartAfter + readMe.slice(badgeStartAfter).indexOf(tag); 14 | const badgeEndAfter = badgeEnd + tag.length; 15 | const fileBefore = readMe.slice(0, badgeStart); 16 | const fileAfter = readMe.slice(badgeEndAfter); 17 | 18 | const fileNext = `${fileBefore}${tag}\n\n${content}\n\n${tag}${fileAfter}`; 19 | await fs.writeFile(fileName, fileNext); 20 | } 21 | 22 | export const VARIABLES_REPLACE_AFTER_TEST_COMMENT = "// Variables to be replaced after tests"; -------------------------------------------------------------------------------- /src/data/user-credential/types.ts: -------------------------------------------------------------------------------- 1 | import {Expiring} from "../expiring"; 2 | import {AuthenticatorTransportFuture} from "@simplewebauthn/typescript-types"; 3 | 4 | export interface UserCredentialData extends Expiring, Record { 5 | userId: string; 6 | credentialId: string; 7 | credentialPublicKey?: string; 8 | credentialCounter?: number; 9 | deviceId?: string; 10 | name?: string; 11 | verifiedAt?: string; 12 | authenticatorUserId?: string; 13 | authenticatorType?: "credential" | "payment" | string; 14 | authenticatorTransports?: AuthenticatorTransportFuture[]; 15 | } 16 | 17 | export interface UserCredential extends UserCredentialData { 18 | userCredentialId: string; 19 | createdAt: string; 20 | updatedAt: string; 21 | } 22 | 23 | export type SetUserCredential = UserCredentialData & Pick & Partial; 24 | 25 | export type UserCredentialIdentifiers = Pick -------------------------------------------------------------------------------- /src/react/client/components/icons/globe-alt.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from "./types"; 2 | 3 | export function GlobeAltIcon(props: IconProps) { 4 | return ( 5 | 14 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/tests/readme/sync/sync.js: -------------------------------------------------------------------------------- 1 | import { addEventListener, sync, caches, index, dispatchEvent } from "@virtualstate/internal"; 2 | 3 | addEventListener("sync", ({ tag, waitUntil }) => { 4 | if (tag === "images") { 5 | waitUntil(onSyncImages()); 6 | } 7 | 8 | async function onSyncImages() { 9 | const cache = await caches.open("contentIndex"); 10 | for (const { id, icons } of await index.getAll()) { 11 | for (const { src } of icons) { 12 | console.log(`Updating icon "${src}" for ${id}`); 13 | await cache.put( 14 | src, 15 | await fetch(src) 16 | ); 17 | } 18 | } 19 | } 20 | }); 21 | 22 | await sync.register("images"); 23 | 24 | // Ran elsewhere by scheduler 25 | // Is usually managed by generateVirtualSyncEvents 26 | await dispatchEvent({ 27 | type: "sync", 28 | tag: "images", 29 | schedule: { 30 | immediate: true 31 | } 32 | }); -------------------------------------------------------------------------------- /src/worker/service-worker/worker-service-url.ts: -------------------------------------------------------------------------------- 1 | import {Config, Service, ImportableURL} from "./configure/types"; 2 | import {ok} from "../../is"; 3 | 4 | export function getMaybeFunctionURL(url: ImportableURL) { 5 | if (typeof url !== "function") { 6 | return url; 7 | } 8 | return `data:text/javascript,${encodeURIComponent(` 9 | const $_IMPORTED_FUNCTION = (${String(url)}); 10 | export default await $_IMPORTED_FUNCTION(self); 11 | `)}` 12 | } 13 | 14 | export function getImportUrlSourceForService(service: Service, config: Config) { 15 | let url = service.url; 16 | if (!url) { 17 | // TODO map to different extensions 18 | url = `./${service.name}.js`; 19 | } 20 | if (!Array.isArray(url)) { 21 | return new URL(getMaybeFunctionURL(url), config.url).toString(); 22 | } 23 | const [first] = url; 24 | ok(first, "Expected at least one url to import for service"); 25 | return new URL(getMaybeFunctionURL(first), config.url).toString(); 26 | } -------------------------------------------------------------------------------- /src/react/client/components/icons/scale.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from "./types"; 2 | 3 | export function ScaleIcon(props: IconProps) { 4 | return ( 5 | 14 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/listen/task/list-tasks.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance, FastifyRequest } from "fastify"; 2 | import { listTasks, taskSchema } from "../../data"; 3 | import { authenticate } from "../authentication"; 4 | import {isUnauthenticated} from "../../authentication"; 5 | 6 | export async function listTaskRoutes(fastify: FastifyInstance) { 7 | const response = { 8 | 200: { 9 | type: "array", 10 | items: taskSchema.task, 11 | }, 12 | }; 13 | 14 | const schema = { 15 | description: "List of tasks", 16 | tags: ["task"], 17 | summary: "", 18 | response, 19 | security: [ 20 | { 21 | apiKey: [] as string[], 22 | }, 23 | ], 24 | }; 25 | 26 | try { 27 | fastify.get("/", { 28 | schema, 29 | preHandler: authenticate(fastify, { anonymous: true }), 30 | async handler(request: FastifyRequest, response) { 31 | response.send(await listTasks({ 32 | public: isUnauthenticated() 33 | })); 34 | }, 35 | }); 36 | } catch { } 37 | } 38 | -------------------------------------------------------------------------------- /src/data/change/types.ts: -------------------------------------------------------------------------------- 1 | import {Expiring} from "../expiring"; 2 | 3 | export type ChangeStatus = "pending" | "applied" | "cancelled" | string; 4 | 5 | export interface ChangeOptionData extends Record { 6 | type?: string; 7 | } 8 | 9 | export interface ChangeTargetType { 10 | type: string; 11 | } 12 | 13 | export interface ChangeTarget extends ChangeTargetType { 14 | id: string; 15 | } 16 | 17 | 18 | export interface ChangeTargetIdentifier { 19 | type: string; 20 | target: ChangeTargetType; 21 | } 22 | 23 | export interface ChangeData extends ChangeTargetIdentifier, Expiring { 24 | target: ChangeTarget; 25 | userId?: string; 26 | options?: ChangeOptionData; 27 | data?: Record; 28 | } 29 | 30 | export interface Change extends ChangeData { 31 | status: ChangeStatus; 32 | changeId: string; 33 | createdAt: string; 34 | updatedAt: string; 35 | appliedAt?: string; 36 | } 37 | 38 | export interface ChangeIdentifier extends ChangeTargetIdentifier { 39 | changeId: string; 40 | } 41 | -------------------------------------------------------------------------------- /src/data/storage/types.ts: -------------------------------------------------------------------------------- 1 | export type MetaRecord = Record; 2 | 3 | export interface AsyncSetStore { 4 | 5 | } 6 | 7 | export interface MetaKeyValueStore extends KeyValueStore { 8 | 9 | } 10 | 11 | export interface KeyValueStoreOptions { 12 | prefix?: string; 13 | counter?: boolean; // To disable set to false 14 | memory?: boolean; 15 | meta?(key?: string): MetaKeyValueStore; // Optional 16 | } 17 | 18 | export interface KeyValueStore extends AsyncIterable { 19 | name: string; 20 | get(key: string): Promise; 21 | set(key: string, value: T): Promise; 22 | values(): Promise; 23 | keys(): Promise; 24 | delete(key: string): Promise; 25 | has(key: string): Promise; 26 | clear(): Promise; 27 | increment(key: string): Promise; 28 | meta(key?: string): MetaKeyValueStore; 29 | } 30 | 31 | export interface KeyValueStoreFn { 32 | (name: string): KeyValueStore 33 | } -------------------------------------------------------------------------------- /src/listen/task/add-task.ts: -------------------------------------------------------------------------------- 1 | import {FastifyInstance} from "fastify"; 2 | import { addTask, TaskData, taskSchema } from "../../data"; 3 | import { authenticate } from "../authentication"; 4 | 5 | export async function addTaskRoutes(fastify: FastifyInstance) { 6 | type Schema = { 7 | Body: TaskData; 8 | }; 9 | 10 | const response = { 11 | 201: { 12 | description: "A new task", 13 | ...taskSchema.task, 14 | }, 15 | }; 16 | 17 | const schema = { 18 | description: "Add a new task", 19 | tags: ["task"], 20 | summary: "", 21 | body: taskSchema.taskData, 22 | response, 23 | security: [ 24 | { 25 | apiKey: [] as string[], 26 | }, 27 | ], 28 | }; 29 | 30 | try { 31 | fastify.post("/", { 32 | schema, 33 | preHandler: authenticate(fastify), 34 | async handler(request, response) { 35 | const task = await addTask(request.body); 36 | response.status(201); 37 | response.send(task); 38 | }, 39 | }); 40 | } catch {} 41 | } 42 | -------------------------------------------------------------------------------- /src/worker/service-worker/transferrable.ts: -------------------------------------------------------------------------------- 1 | import {FileHandle} from "node:fs/promises"; 2 | import {X509Certificate} from "node:crypto"; 3 | import {Blob} from "node:buffer"; 4 | import {MessagePort} from "worker_threads"; 5 | 6 | type TransferListItem = ArrayBuffer | MessagePort | FileHandle | X509Certificate | Blob; 7 | 8 | export function isTransferable(object: unknown): object is TransferListItem { 9 | return ( 10 | object instanceof ArrayBuffer || 11 | object instanceof ReadableStream || 12 | object instanceof TransformStream || 13 | object instanceof WritableStream 14 | ) 15 | } 16 | 17 | export function listTransferable(object: unknown): TransferListItem[] { 18 | if (!object) return []; 19 | if (Array.isArray(object)) { 20 | return object.flatMap(listTransferable); 21 | } 22 | if (isTransferable(object)) { 23 | return [object]; 24 | } 25 | if (typeof object !== "object") { 26 | return []; 27 | } 28 | return Object.values(object).flatMap(listTransferable) 29 | } -------------------------------------------------------------------------------- /src/data/durable-request/set-durable-request.ts: -------------------------------------------------------------------------------- 1 | import {v4} from "uuid"; 2 | import {DurableRequest, PartialDurableRequest} from "./types"; 3 | import {getDurableRequestStore} from "./store"; 4 | import {DurableEventData} from "../durable-event"; 5 | import {getDurableRequestIdForEvent} from "./get-durable-request"; 6 | 7 | 8 | export async function setDurableRequest(data: PartialDurableRequest) { 9 | const createdAt = new Date().toISOString(); 10 | const durableRequestId = data.durableRequestId || v4(); 11 | const durableRequest: DurableRequest = { 12 | ...data, 13 | createdAt, 14 | updatedAt: createdAt, 15 | durableRequestId, 16 | }; 17 | const store = getDurableRequestStore(); 18 | await store.set(durableRequestId, durableRequest); 19 | return durableRequest; 20 | } 21 | 22 | export function setDurableRequestForEvent(data: PartialDurableRequest, event: DurableEventData) { 23 | return setDurableRequest({ 24 | ...data, 25 | durableRequestId: getDurableRequestIdForEvent(event) 26 | }); 27 | } -------------------------------------------------------------------------------- /src/react/client/components/icons/calendar.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from "./types"; 2 | 3 | export function CalendarIcon(props: IconProps) { 4 | return ( 5 | 14 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /scripts/git-info.js: -------------------------------------------------------------------------------- 1 | import gitCommitInfo from "git-commit-info"; 2 | 3 | export const info = gitCommitInfo(); 4 | export const date = new Date(info.date) 5 | 6 | export const msSinceCommit = Date.now() - date.getTime(); 7 | export const secondsBetweenCommitAndBuildValue = msSinceCommit / 1000; 8 | export const minutesBetweenCommitAndBuildValue = secondsBetweenCommitAndBuildValue / 60; 9 | export const secondsBetweenCommitAndBuild = toNumberRoundedString(secondsBetweenCommitAndBuildValue); 10 | export const minutesBetweenCommitAndBuild = toNumberRoundedString(minutesBetweenCommitAndBuildValue); 11 | 12 | export const minutesFloored = Math.floor(minutesBetweenCommitAndBuildValue) 13 | export const secondsWithoutMinutes = Math.floor(secondsBetweenCommitAndBuildValue - (minutesFloored * 60)); 14 | export const secondsTime = secondsWithoutMinutes > 1 ? ` and ${secondsWithoutMinutes} seconds` : ""; 15 | export const timeBetweenCommitAndBuild = `${minutesFloored} minutes${secondsTime}` 16 | 17 | function toNumberRoundedString(value) { 18 | return Math.round(value * 100) / 100 19 | } -------------------------------------------------------------------------------- /src/data/authentication-role/types.ts: -------------------------------------------------------------------------------- 1 | import {Expiring} from "../expiring"; 2 | 3 | export type SystemRole = "system"; 4 | 5 | declare global { 6 | interface AuthenticationRoles extends Record { 7 | 8 | } 9 | } 10 | 11 | export type AuthenticationRole = 12 | | "moderator" 13 | | "admin" 14 | | "owner" 15 | | "member" 16 | | "booster" 17 | | "industry" 18 | | "developer" 19 | | "coordinator" 20 | | "partner" 21 | | "anonymous" 22 | | SystemRole 23 | // Allows typing of authentication roles from the global scope. 24 | // keys from multiple interface definitions in global will merge together 25 | | keyof AuthenticationRoles; 26 | 27 | export interface UserAuthenticationRoleData extends Expiring { 28 | userId: string; 29 | roles: AuthenticationRole[]; 30 | } 31 | 32 | export interface UserAuthenticationRole extends UserAuthenticationRoleData { 33 | createdAt: string; 34 | updatedAt: string; 35 | } 36 | 37 | export type PartialUserAuthenticationRole = UserAuthenticationRoleData & Partial -------------------------------------------------------------------------------- /src/data/system-log/schema.ts: -------------------------------------------------------------------------------- 1 | export const systemLogData = { 2 | type: "object", 3 | properties: { 4 | uniqueCode: { 5 | type: "string", 6 | nullable: true 7 | }, 8 | value: { 9 | type: "string", 10 | nullable: true 11 | }, 12 | action: { 13 | type: "string", 14 | nullable: true 15 | }, 16 | partnerId: { 17 | type: "string", 18 | nullable: true 19 | }, 20 | message: { 21 | type: "string" 22 | }, 23 | }, 24 | required: [ 25 | "message" 26 | ] 27 | } as const; 28 | 29 | export const systemLog = { 30 | type: "object", 31 | properties: { 32 | systemLogId: { 33 | type: "string" 34 | }, 35 | ...systemLogData.properties, 36 | timestamp: { 37 | type: "string" 38 | } 39 | }, 40 | required: [ 41 | ...systemLogData.required, 42 | "systemLogId", 43 | "timestamp" 44 | ] 45 | } as const; -------------------------------------------------------------------------------- /src/data/user-credential/delete-user-credential.ts: -------------------------------------------------------------------------------- 1 | import {DEFAULT_CREDENTIAL_EXPIRES_IN_MS, getUserCredentialStore} from "./store"; 2 | import {UserCredential, SetUserCredential, UserCredentialIdentifiers} from "./types"; 3 | import {v4} from "uuid"; 4 | import {createHash} from "crypto"; 5 | import {getExpiresAt} from "../expiring-kv"; 6 | 7 | const { 8 | IS_LOCAL, 9 | IS_DEMO 10 | } = process.env; 11 | 12 | export async function deleteUserCredential(data: UserCredentialIdentifiers): Promise { 13 | // Only local and demo can delete for now 14 | // until operation authz is implemented https://github.com/opennetwork/logistics/issues/39 15 | if (!(IS_LOCAL || IS_DEMO)) { 16 | return; 17 | } 18 | const store = await getUserCredentialStore(data.userId); 19 | await store.delete(data.userCredentialId); 20 | } 21 | 22 | export async function deleteUserCredentials(items: UserCredentialIdentifiers[]): Promise { 23 | // Delete in serial... it's not needing to be fast right now 24 | for (const data of items) { 25 | await deleteUserCredential(data) 26 | } 27 | } -------------------------------------------------------------------------------- /src/data/user/delete-user.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExternalUserReferenceData, 3 | } from "./types"; 4 | import {getExternalReferenceKey, getExternalUserReferenceStore, getUserStore} from "./store"; 5 | import {getExternalReference} from "./get-user"; 6 | import {AuthenticationStateType} from "../authentication-state"; 7 | 8 | export async function deleteUser(userId: string) { 9 | const store = getUserStore(); 10 | await store.delete(userId); 11 | } 12 | 13 | export async function deleteExternalUser( 14 | data: ExternalUserReferenceData 15 | ) { 16 | const { externalId, externalType } = data; 17 | const reference = await getExternalReference(externalType, externalId); 18 | if (!reference) return; 19 | await deleteUser(reference.userId); 20 | await deleteExternalReference(externalType, externalId); 21 | } 22 | 23 | export async function deleteExternalReference(externalType: AuthenticationStateType, externalId: string) { 24 | const store = getExternalUserReferenceStore(); 25 | const key = getExternalReferenceKey(externalType, externalId); 26 | await store.delete(key); 27 | } 28 | -------------------------------------------------------------------------------- /src/worker/service-worker/worker-loader.ts: -------------------------------------------------------------------------------- 1 | export interface ResolveOptions { 2 | parentURL?: string; 3 | } 4 | 5 | export interface ResolveFn { 6 | (url: string, context: ResolveOptions): unknown 7 | } 8 | 9 | export function resolve(url: string, context: ResolveOptions, next: ResolveFn) { 10 | console.log({ resolve: url }); 11 | return next(url, context); 12 | // 13 | // if (url.startsWith("node:") || url.startsWith("data:")) { 14 | // return next(url, context); 15 | // } 16 | // 17 | // 18 | // try { 19 | // return next(url, context); 20 | // } catch (error) { 21 | // console.log("Loader error", error); 22 | // return { 23 | // shortCircuit: true, 24 | // url 25 | // } 26 | // } 27 | } 28 | 29 | export async function load(url: string, context: ResolveOptions, next: ResolveFn) { 30 | try { 31 | 32 | console.log({ load: url }); 33 | 34 | return next(url, context); 35 | } catch (error) { 36 | console.log("Loader error", error); 37 | throw error; 38 | } 39 | } -------------------------------------------------------------------------------- /src/data/authentication-state/add-authentication-state.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AuthenticationStateData, 3 | } from "./types"; 4 | import { 5 | DEFAULT_COOKIE_STATE_EXPIRES_MS, 6 | } from "./store"; 7 | import { getExpiresAt } from "../expiring-kv"; 8 | import { setAuthenticationState } from "./set-authentication-state"; 9 | import {addAnonymousUser} from "../user"; 10 | 11 | export async function addCookieState(data: Partial) { 12 | return addAuthenticationState({ 13 | ...data, 14 | type: "cookie", 15 | expiresAt: getExpiresAt(DEFAULT_COOKIE_STATE_EXPIRES_MS, data.expiresAt), 16 | }); 17 | } 18 | 19 | export async function addAuthenticationState(data: AuthenticationStateData) { 20 | return setAuthenticationState(data); 21 | } 22 | 23 | export async function addAnonymousCookieState() { 24 | const user = await addAnonymousUser(); 25 | return addCookieState({ 26 | userId: user.userId, 27 | roles: [ 28 | "anonymous" 29 | ], 30 | from: { 31 | type: "anonymous", 32 | createdAt: new Date().toISOString() 33 | } 34 | }); 35 | } -------------------------------------------------------------------------------- /src/react/client/components/icons/calendar-days.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from "./types"; 2 | 3 | export function CalendarDaysIcon(props: IconProps) { 4 | return ( 5 | 14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/react/client/components/icons/prescription-bottle.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from "./types"; 2 | 3 | export function PrescriptionBottleIcon(props: IconProps) { 4 | return ( 5 | 14 | {/* Transform it slighly smaller to make it fit nicer */} 15 | 16 | {/* Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) */} 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/data/durable-event/types.ts: -------------------------------------------------------------------------------- 1 | import {Expiring} from "../expiring"; 2 | 3 | export interface DurableEventSchedule { 4 | timezone?: string; 5 | // For cases where we want an event triggered after a specific time 6 | after?: string; 7 | // For cases where we want an event triggered before a specific time 8 | before?: string; 9 | immediate?: boolean; 10 | cron?: string; 11 | delay?: number | string; 12 | // Once delay has completed, repeat 13 | repeat?: boolean; 14 | } 15 | 16 | export interface UnknownEvent { 17 | type: unknown; 18 | } 19 | 20 | export interface DurableEventTypeData extends UnknownEvent { 21 | type: string; 22 | } 23 | 24 | export interface DurableEventData extends Record, DurableEventTypeData, Expiring { 25 | timeStamp?: number; 26 | durableEventId?: string; 27 | schedule?: DurableEventSchedule; 28 | retain?: boolean; 29 | virtual?: boolean; 30 | serviceWorkerId?: string; 31 | createdAt?: string; 32 | } 33 | 34 | export interface DurableEvent extends DurableEventData { 35 | durableEventId: string 36 | } -------------------------------------------------------------------------------- /src/listen/body-parser.ts: -------------------------------------------------------------------------------- 1 | import qs from "qs"; 2 | 3 | function isRecordLike(value: unknown): value is Record { 4 | return typeof value === "object"; 5 | } 6 | 7 | export function processStringFields(value: unknown): unknown { 8 | if (!value) return value; 9 | 10 | if (Array.isArray(value)) { 11 | return value.map((item) => processStringFields(item)); 12 | } 13 | 14 | if (!isRecordLike(value)) { 15 | return value; 16 | } 17 | 18 | return Object.fromEntries( 19 | Object.entries(value).map((entry) => { 20 | const [key, value] = entry; 21 | if (key.endsWith("_boolean")) { 22 | return [key.replace(/_boolean$/, ""), isBooleanLike(value)]; 23 | } 24 | return [key, processStringFields(value)]; 25 | }) 26 | ); 27 | } 28 | 29 | function isBooleanLike(value: unknown) { 30 | return value === "1" || value === "true" || value === "on"; 31 | } 32 | 33 | export function parseStringFields(string: string): unknown { 34 | const parsed = qs.parse(string, { 35 | allowDots: true, 36 | }); 37 | return processStringFields(parsed); 38 | } 39 | -------------------------------------------------------------------------------- /src/tests/worker/service-worker/store/patch.ts: -------------------------------------------------------------------------------- 1 | import {requestMethod} from "../routes.example"; 2 | import {cache} from "./cache"; 3 | import {isMaybeJSONContentType} from "./is"; 4 | 5 | requestMethod.patch({ pathname: "/:type/:id" }, async request => { 6 | const match = await cache.match(request.url); 7 | if (!match) return new Response(null, { status: 404 }); 8 | 9 | if (isMaybeJSONContentType(match) && isMaybeJSONContentType(request)) { 10 | const before = await match.json(); 11 | const update = await request.json(); 12 | const after = { 13 | ...before, 14 | ...update 15 | }; 16 | const response = Response.json(after, { 17 | headers: { 18 | "Last-Modified": new Date().toUTCString() 19 | } 20 | }) 21 | await cache.put(request.url, response.clone()); 22 | return response; 23 | } else { 24 | console.log(match.headers, request.headers) 25 | return new Response("Unable to patch non JSON content", { 26 | status: 400 27 | }); 28 | } 29 | }) -------------------------------------------------------------------------------- /src/scheduler/container.ts: -------------------------------------------------------------------------------- 1 | export interface SchedulerFn { 2 | (): void | Promise 3 | } 4 | 5 | export type SchedulerTaskPriority = "user-blocking" | "user-visible" | "background"; 6 | 7 | // TODO 8 | export interface TaskSignal extends AbortSignal { 9 | priority: SchedulerTaskPriority 10 | } 11 | 12 | export interface SchedulerTaskOptions { 13 | priority?: SchedulerTaskPriority | string; 14 | signal?: AbortSignal | TaskSignal; 15 | delay?: number; 16 | } 17 | 18 | export class Scheduler { 19 | 20 | async postTask(callback: SchedulerFn, options?: SchedulerTaskOptions) { 21 | if (options?.delay) { 22 | await this.wait(options.delay) 23 | } 24 | if (options?.signal?.aborted) { 25 | throw new Error("Aborted"); 26 | } 27 | return callback(); 28 | } 29 | 30 | async wait(delay: number | string) { 31 | if (typeof delay === "string") { 32 | // TODO wait for event 33 | } else { 34 | return new Promise(resolve => setTimeout(resolve, delay)); 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/data/storage/redis-memory.ts: -------------------------------------------------------------------------------- 1 | import type { RedisMemoryServer } from "redis-memory-server"; 2 | 3 | export function isRedisMemory() { 4 | return !!process.env.REDIS_MEMORY; 5 | } 6 | 7 | let redisServer: RedisMemoryServer | undefined = undefined, 8 | initialURL: string | undefined = undefined; 9 | 10 | export async function stopRedisMemory() { 11 | if (!redisServer) return; 12 | await redisServer.stop(); 13 | process.env.REDIS_URL = initialURL ?? ""; 14 | redisServer = undefined; 15 | initialURL = undefined; 16 | } 17 | 18 | export async function startRedisMemory() { 19 | if (redisServer) return; 20 | const mod = await import("redis-memory-server").catch(() => undefined); 21 | if (!mod) { 22 | console.warn("Redis memory enabled but redis-memory-server not installed"); 23 | return; 24 | } 25 | const { RedisMemoryServer } = mod; 26 | redisServer = new RedisMemoryServer(); 27 | 28 | const host = await redisServer.getHost(); 29 | const port = await redisServer.getPort(); 30 | 31 | initialURL = process.env.REDIS_URL; 32 | process.env.REDIS_URL = `redis://${host}:${port}`; 33 | } 34 | -------------------------------------------------------------------------------- /src/data/attendee/schema.ts: -------------------------------------------------------------------------------- 1 | export const attendeeData = { 2 | type: "object", 3 | properties: { 4 | reference: { 5 | type: "string" 6 | }, 7 | name: { 8 | type: "string", 9 | nullable: true 10 | }, 11 | email: { 12 | type: "string", 13 | nullable: true 14 | }, 15 | attendeeId: { 16 | type: "string", 17 | nullable: true 18 | }, 19 | createdAt: { 20 | type: "string", 21 | nullable: true 22 | }, 23 | }, 24 | additionalProperties: true, 25 | required: [ 26 | "reference" 27 | ] 28 | } 29 | 30 | export const attendee = { 31 | type: "object", 32 | properties: { 33 | ...attendeeData.properties, 34 | attendeeId: { 35 | type: "string", 36 | }, 37 | createdAt: { 38 | type: "string", 39 | }, 40 | }, 41 | additionalProperties: true, 42 | required: [ 43 | ...attendeeData.required, 44 | "attendeeId", 45 | "createdAt" 46 | ] 47 | } -------------------------------------------------------------------------------- /src/react/client/pages/paths/index.ts: -------------------------------------------------------------------------------- 1 | async function productList() { 2 | const { productList: fn } = await import("./product-list"); 3 | return fn(); 4 | } 5 | 6 | async function home() { 7 | const { home: fn } = await import("./home"); 8 | return fn(); 9 | } 10 | 11 | async function login() { 12 | const { login: fn } = await import("./login"); 13 | return fn(); 14 | } 15 | 16 | async function paymentMethodSelect() { 17 | const { paymentMethodSelect: fn } = await import("./payment-method-select"); 18 | return fn(); 19 | } 20 | 21 | async function orderConfirmation() { 22 | const { orderConfirmation: fn } = await import("./order-confirmation"); 23 | return fn(); 24 | } 25 | 26 | export const paths: Record void | Promise> = { 27 | "/": home, 28 | "/home": home, 29 | "/login": login, 30 | "/order/checkout/confirmation": orderConfirmation, 31 | "/payment-method/select": paymentMethodSelect, 32 | "/products": productList 33 | }; 34 | 35 | export function runPath() { 36 | const { pathname } = window.location; 37 | const pathFn = paths[pathname]; 38 | if (!pathFn) return; 39 | return pathFn(); 40 | } 41 | -------------------------------------------------------------------------------- /src/react/client/components/icons/globe.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from "./types"; 2 | 3 | export function GlobeIcon(props: IconProps) { 4 | return ( 5 | 14 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Axiom Applied Technologies and Development Limited 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/data/partner/add-partner.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from "uuid"; 2 | import { createPartnerAccessToken } from "../access-token"; 3 | import { Partner, PartnerData } from "./types"; 4 | import { getPartnerStore } from "./store"; 5 | import { addOrganisation } from "../organisation"; 6 | 7 | export interface AddPartnerInput extends PartnerData {} 8 | 9 | export async function addPartner(data: AddPartnerInput): Promise { 10 | const store = getPartnerStore(); 11 | const partnerId = v4(); 12 | const { partnerName, ...organisationData } = data; 13 | const { organisationId } = await addOrganisation({ 14 | ...organisationData, 15 | organisationName: partnerName, 16 | partnerId, 17 | }); 18 | const createdAt = new Date().toISOString(); 19 | const partner: Partner = { 20 | partnerName, 21 | partnerId, 22 | organisationId, 23 | approved: false, 24 | approvedAt: undefined, 25 | approvedByUserId: undefined, 26 | createdAt, 27 | updatedAt: createdAt, 28 | }; 29 | await store.set(partnerId, partner); 30 | const { accessToken } = await createPartnerAccessToken(partnerId); 31 | return { 32 | ...partner, 33 | accessToken, 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/data/attendee/get-referenced-attendees.ts: -------------------------------------------------------------------------------- 1 | import {setAttendee} from "./set-attendee"; 2 | import {Attendee, AttendeeData} from "./types"; 3 | 4 | export async function createAttendeeReferences(input: (string | AttendeeData)[]): Promise { 5 | const attendeeInput = parseAttendeeReferences(input); 6 | return attendeeInput.length ? await Promise.all( 7 | attendeeInput.map(setAttendee) 8 | ) : []; 9 | } 10 | 11 | export function getAttendeeReferenceMap(attendees: Attendee[]) { 12 | return new Map( 13 | attendees.map(attendee => [attendee.reference, attendee]) 14 | ); 15 | } 16 | 17 | export function parseAttendeeReferences(attendees: (AttendeeData | string)[]): AttendeeData[] { 18 | return [ 19 | ...(attendees ?? []).map(attendee => { 20 | if (typeof attendee === "string") { 21 | return { reference: attendee } 22 | } 23 | return attendee; 24 | }) 25 | ] 26 | .filter( 27 | (value, index, array) => { 28 | const before = array.slice(0, index); 29 | return !before.find(other => other.reference === value.reference); 30 | } 31 | ) 32 | } -------------------------------------------------------------------------------- /src/listen/background.ts: -------------------------------------------------------------------------------- 1 | import {FastifyInstance} from "fastify"; 2 | import {background} from "../background"; 3 | import {authenticateSignature} from "./authentication"; 4 | 5 | export async function backgroundRoutes(fastify: FastifyInstance) { 6 | 7 | try { 8 | fastify.get("/background", { 9 | async handler(request, response) { 10 | 11 | await background({ 12 | query: request.query 13 | }); 14 | 15 | response.status(200); 16 | response.send(); 17 | 18 | } 19 | }); 20 | } catch {} 21 | 22 | try { 23 | fastify.post("/event", { 24 | config: { 25 | rawBody: true 26 | }, 27 | preHandler: authenticateSignature(fastify, { internal: true }), 28 | async handler(request, response) { 29 | 30 | console.log("Event received", request.body) 31 | 32 | await background({ 33 | query: request.body 34 | }); 35 | 36 | response.status(200); 37 | response.send(); 38 | 39 | } 40 | }); 41 | } catch {} 42 | 43 | } -------------------------------------------------------------------------------- /src/react/server/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactData, DataProvider } from "./data"; 2 | import { getOrigin } from "../../listen/config"; 3 | import { UnauthenticatedLayout, Layout, LayoutProps } from "./layout"; 4 | import {View} from "../../view"; 5 | 6 | export * from "./data"; 7 | export * from "./paths"; 8 | export * from "./layout"; 9 | 10 | export interface OpenNetworkServerProps extends ReactData { 11 | url: string; 12 | view: View; 13 | } 14 | 15 | export default function OpenNetworkServer(options: OpenNetworkServerProps) { 16 | const { pathname } = new URL(options.url, getOrigin()); 17 | const { view: { Component } } = options; 18 | 19 | if (!Component) { 20 | return
Could not find {pathname}
; 21 | } 22 | 23 | let children = ; 24 | 25 | if (!options.isFragment) { 26 | const layoutProps: LayoutProps = { 27 | url: options.url, 28 | }; 29 | if (options.isUnauthenticated) { 30 | children = {children}; 31 | } else { 32 | children = {children}; 33 | } 34 | } 35 | 36 | return {children}; 37 | } 38 | -------------------------------------------------------------------------------- /src/tests/worker/service-worker/store/get.ts: -------------------------------------------------------------------------------- 1 | import {requestMethod} from "../routes.example"; 2 | import {cache} from "./cache"; 3 | 4 | requestMethod.get({ pathname: "/" }, async () => { 5 | const keys = await cache.keys(); 6 | const items = keys 7 | .filter(key => key.method === "GET") 8 | .map(key => ({ 9 | url: key.url, 10 | headers: { 11 | "Content-Type": key.headers.get("Content-Type") 12 | } 13 | })); 14 | return Response.json(items); 15 | }); 16 | 17 | requestMethod.get({ pathname: "/:type" }, async request => { 18 | const keys = await cache.keys(); 19 | const items = keys 20 | .filter(key => key.method === "GET") 21 | .filter(key => key.url.startsWith(request.url)) 22 | .map(key => ({ 23 | url: key.url, 24 | headers: { 25 | "Content-Type": key.headers.get("Content-Type") 26 | } 27 | })); 28 | return Response.json(items); 29 | 30 | }) 31 | 32 | requestMethod.get({ pathname: "/:type/:id" }, async request => { 33 | const match = await cache.match(request); 34 | return match ?? new Response(null, { 35 | status: 404 36 | }); 37 | }) -------------------------------------------------------------------------------- /src/data/seed/seed.ts: -------------------------------------------------------------------------------- 1 | import * as happening from "./happening"; 2 | import { Seed, SeedOptions } from "./type"; 3 | import { ok } from "../../is"; 4 | import {getConfig} from "../../config"; 5 | 6 | export type Seeds = Record; 7 | 8 | export interface SeedConfig { 9 | seeds?: Seeds; 10 | } 11 | 12 | export const DEFAULT_SEEDS: Seeds = { 13 | happening, 14 | }; 15 | 16 | export const DEFAULT_SEED = "happening"; 17 | 18 | export async function seed(options?: SeedOptions) { 19 | const name = options?.seed || DEFAULT_SEED; 20 | const { seeds: givenSeeds } = getConfig(); 21 | const seeds = { 22 | ...DEFAULT_SEEDS, 23 | ...givenSeeds 24 | } 25 | const value = seeds[name]; 26 | if (!value) { 27 | console.warn(`Expected seed name ${name} to be available, seeds: ${Object.keys(seeds)}`); 28 | return; 29 | } 30 | await value.seed({ 31 | ...options, 32 | seed: name, 33 | }); 34 | } 35 | 36 | export async function autoSeed() { 37 | const { ENABLE_SEED } = process.env; 38 | if (!ENABLE_SEED?.length) return; 39 | if (ENABLE_SEED === "true" || ENABLE_SEED === "1") { 40 | return await seed(); 41 | } 42 | return await seed({ 43 | seed: ENABLE_SEED, 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /src/data/user/add-user.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExternalUserReferenceData, 3 | User, 4 | UserData, 5 | } from "./types"; 6 | import {resetUserExpiryWithType, setExternalReference, setUser} from "./set-user"; 7 | import {getExternalUser} from "./get-user"; 8 | 9 | export async function addUser(data: UserData) { 10 | return setUser(data); 11 | } 12 | 13 | export async function addExternalUser( 14 | data: UserData & ExternalUserReferenceData, 15 | existingUser?: User 16 | ) { 17 | const { externalId, externalType, ...rest } = data; 18 | 19 | let user = existingUser ?? await addUser({ 20 | ...rest, 21 | externalType, 22 | }); 23 | 24 | if (existingUser && user.externalType !== externalType && user.externalType === "anonymous") { 25 | user = await resetUserExpiryWithType(user, externalType); 26 | } 27 | 28 | await setExternalReference({ 29 | externalType, 30 | externalId, 31 | userId: user.userId, 32 | expiresAt: user.expiresAt, 33 | }); 34 | 35 | return user; 36 | } 37 | 38 | export async function addAnonymousUser() { 39 | const user = await addUser({ 40 | externalType: "anonymous", 41 | }); 42 | await getExternalUser(user.externalType, user.userId, user); 43 | return user; 44 | } -------------------------------------------------------------------------------- /src/sync/virtual.ts: -------------------------------------------------------------------------------- 1 | import {SyncDurableEventData} from "./dispatch"; 2 | import {virtual} from "../events/virtual/virtual"; 3 | import {DurableSyncManager, getSyncTagStore} from "./manager"; 4 | 5 | 6 | export async function * generateVirtualSyncEvents(manager?: DurableSyncManager): AsyncIterable { 7 | const store = getSyncTagStore(); 8 | for await (const { tag, lastChance } of manager) { 9 | yield { 10 | // Utilise a durableEventId so that a lock is created per tag 11 | durableEventId: `${store.name}:${tag}`, 12 | type: "sync", 13 | tag, 14 | lastChance, 15 | virtual: true 16 | }; 17 | 18 | if (lastChance) { 19 | const existing = await store.get(tag); 20 | if (existing) 21 | // TODO is this what lastChance mean for durable syncing... is it lastChance for this process? 22 | // 23 | // Returns true if the user agent will not make further synchronization attempts after the current attempt. 24 | await store.delete(tag); 25 | } 26 | } 27 | } 28 | 29 | export const removeSyncVirtualFunction = virtual(generateVirtualSyncEvents); -------------------------------------------------------------------------------- /src/content-index/dispatch.ts: -------------------------------------------------------------------------------- 1 | import {dispatcher} from "../events/schedule/schedule"; 2 | import {createWaitUntil} from "../fetch"; 3 | import {isSignalled} from "../is"; 4 | 5 | export const removeContentDeleteDispatchFunction = dispatcher("contentdelete", async (event, dispatch) => { 6 | const { signal, controller } = getSignal(); 7 | const { 8 | wait, 9 | waitUntil 10 | } = createWaitUntil(); 11 | try { 12 | await dispatch({ 13 | ...event, 14 | signal, 15 | waitUntil 16 | }); 17 | await wait(); 18 | } catch (error) { 19 | if (!signal.aborted) { 20 | controller?.abort(error); 21 | } 22 | throw await Promise.reject(error); 23 | } finally { 24 | if (!signal.aborted) { 25 | controller?.abort(); 26 | } 27 | await wait(); 28 | } 29 | 30 | function getSignal() { 31 | if (isSignalled(event)) { 32 | return { signal: event.signal, controller: undefined } as const; 33 | } 34 | const controller = new AbortController(); 35 | return { 36 | signal: controller.signal, 37 | controller 38 | } as const; 39 | } 40 | }) -------------------------------------------------------------------------------- /src/tests/worker/service-worker/configured/config.data.ts: -------------------------------------------------------------------------------- 1 | import {Config} from "../../../../worker/service-worker/configure/types"; 2 | 3 | export const config: Config = { 4 | services: [ 5 | { 6 | name: "specific-named-data-service", 7 | url: `data:text/javascript,${encodeURIComponent(` 8 | addEventListener("fetch", event => event.respondWith( 9 | Response.json({ 10 | key: "value", 11 | message: "Hello from a specific data worker" 12 | }) 13 | )); 14 | `)}` 15 | }, 16 | { 17 | name: "example", 18 | url: `data:text/javascript,${encodeURIComponent(` 19 | addEventListener("fetch", event => event.respondWith( 20 | fetch("data-service") 21 | )); 22 | `)}`, 23 | bindings: [ 24 | { 25 | name: "data-service", 26 | service: "specific-named-data-service" 27 | } 28 | ] 29 | } 30 | ], 31 | sockets: [ 32 | { 33 | service: "example", 34 | address: "*:3010" 35 | }, 36 | ] 37 | } -------------------------------------------------------------------------------- /src/data/change/schema.ts: -------------------------------------------------------------------------------- 1 | export const changeOptionData = { 2 | type: "object", 3 | properties: { 4 | type: { 5 | type: "string", 6 | nullable: true, 7 | } 8 | }, 9 | additionalProperties: true 10 | } 11 | 12 | export const changeTarget = { 13 | type: "object", 14 | properties: { 15 | type: { 16 | type: "string" 17 | }, 18 | id: { 19 | type: "string" 20 | } 21 | }, 22 | required: ["type", "id"] 23 | }; 24 | 25 | export const changeData = { 26 | type: "object", 27 | properties: { 28 | options: { 29 | type: "array", 30 | items: changeOptionData, 31 | nullable: true, 32 | }, 33 | data: { 34 | type: "object", 35 | properties: {}, 36 | additionalProperties: true 37 | } 38 | } 39 | }; 40 | 41 | export const change = { 42 | type: "object", 43 | properties: { 44 | type: { 45 | type: "string" 46 | }, 47 | target: changeTarget, 48 | changeId: { 49 | type: "string", 50 | }, 51 | createdAt: { 52 | type: "string", 53 | }, 54 | updatedAt: { 55 | type: "string", 56 | }, 57 | ...changeData.properties 58 | }, 59 | required: ["changeId", "createdAt", "updatedAt", "type", "target"], 60 | } as const; 61 | -------------------------------------------------------------------------------- /src/tests/worker/service-worker/configured/config.ts: -------------------------------------------------------------------------------- 1 | import {Config} from "../../../../worker/service-worker/configure/types"; 2 | 3 | // For example 4 | const IS_DEVELOPMENT = true 5 | 6 | export const config: Config = { 7 | services: [ 8 | { 9 | name: "prices", 10 | url: "./prices.js" 11 | }, 12 | { 13 | name: "products", 14 | url: "./products.js" 15 | }, 16 | { 17 | name: "offers", 18 | url: "./offers.js", 19 | bindings: [ 20 | { 21 | protocol: "products", 22 | service: "products" 23 | }, 24 | { 25 | protocol: "prices", 26 | service: "prices" 27 | } 28 | ] 29 | } 30 | ], 31 | sockets: [ 32 | ...(IS_DEVELOPMENT ? [ 33 | { 34 | service: "prices", 35 | address: "*:3010" 36 | }, 37 | { 38 | service: "products", 39 | address: "*:3011" 40 | } 41 | ] : []), 42 | { 43 | service: "offers", 44 | address: "*:3000" 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /src/data/access-token/create-access-token.ts: -------------------------------------------------------------------------------- 1 | import {v4} from "uuid"; 2 | import {createHash} from "crypto"; 3 | import {AccessToken, AccessTokenData, AccessTokenType} from "./types"; 4 | import {getAccessTokenStore} from "./store"; 5 | 6 | // TODO: is this the best way to create an access token string? 7 | function createAccessTokenString(type?: AccessTokenType | string) { 8 | const value = v4(); 9 | const hash = createHash("sha512"); 10 | hash.update(value); 11 | const buffer = hash.digest(); 12 | const hex = buffer.toString("hex"); 13 | if (!type) return hex; 14 | return `${type}_${hex}`; 15 | } 16 | 17 | export async function createPartnerAccessToken(partnerId: string) { 18 | return createAccessToken({ 19 | partnerId, 20 | accessTokenType: "partner" 21 | }); 22 | } 23 | 24 | export async function createAccessToken(data: AccessTokenData) { 25 | const createdAt = new Date().toISOString(); 26 | const accessToken = createAccessTokenString(data.accessTokenType); 27 | const token: AccessToken = { 28 | ...data, 29 | accessToken, 30 | createdAt 31 | }; 32 | const store = getAccessTokenStore(); 33 | await store.set(accessToken, token); 34 | return { 35 | accessToken 36 | } as const; 37 | } -------------------------------------------------------------------------------- /src/data/durable-request/delete-durable-request.ts: -------------------------------------------------------------------------------- 1 | import {getDurableRequestStore} from "./store"; 2 | import {DurableRequestData} from "./types"; 3 | import {isDurableBody} from "./is"; 4 | import {unlink} from "../file/unlink"; 5 | 6 | export async function deleteDurableRequest(durableRequestId: string) { 7 | const store = getDurableRequestStore(); 8 | const existing = await store.get(durableRequestId); 9 | if (!existing) { 10 | return; 11 | } 12 | await deleteDurableRequestBody(existing); 13 | return store.delete(durableRequestId); 14 | } 15 | 16 | export async function deleteDurableRequestBody(durableRequest: DurableRequestData) { 17 | const fileIds = new Set(); 18 | if (isDurableBody(durableRequest.body) && durableRequest.body.type === "file" && typeof durableRequest.body.value === "string") { 19 | fileIds.add(durableRequest.body.value); 20 | } 21 | if (isDurableBody(durableRequest.response?.body) && durableRequest.response.body.type === "file" && typeof durableRequest.response.body.value === "string") { 22 | fileIds.add(durableRequest.response.body.value); 23 | } 24 | if (!fileIds.size) { 25 | return; 26 | } 27 | await Promise.all( 28 | [...fileIds].map(id => unlink(id)) 29 | ); 30 | } -------------------------------------------------------------------------------- /src/data/durable-request/types.ts: -------------------------------------------------------------------------------- 1 | import {Expiring} from "../expiring"; 2 | 3 | export type DurableBodyLike = string | DurableBody; 4 | 5 | export interface DurableRequestData extends Expiring { 6 | url: string; 7 | method?: string; 8 | headers?: Record 9 | body?: DurableBodyLike; 10 | response?: DurableResponseData; 11 | } 12 | 13 | export interface DurableResponseCache { 14 | name: string; 15 | } 16 | 17 | export type BodySource = BodyInit & ( 18 | | ArrayBuffer 19 | | ReadableStream 20 | ) 21 | 22 | export interface DurableBody { 23 | type: "file" | "base64" | "cache" | "source"; 24 | value: string | BodySource; 25 | url?: string; 26 | } 27 | 28 | export interface DurableResponseData extends Pick { 29 | headers?: Record 30 | body?: DurableBodyLike; 31 | } 32 | 33 | export interface DurableRequest extends DurableRequestData { 34 | durableRequestId: string; 35 | createdAt: string; 36 | updatedAt: string; 37 | } 38 | 39 | export interface RequestQueryInfo extends DurableRequestData { 40 | 41 | } 42 | 43 | export type RequestQuery = RequestQueryInfo | RequestInfo | URL 44 | 45 | export type PartialDurableRequest = DurableRequestData & Partial; -------------------------------------------------------------------------------- /src/tests/readme/periodic-sync/periodic-sync.js: -------------------------------------------------------------------------------- 1 | import { addEventListener, periodicSync, caches, index, dispatchEvent } from "@virtualstate/internal"; 2 | 3 | addEventListener("periodicsync", ({ tag, waitUntil }) => { 4 | if (tag === "images") { 5 | waitUntil(onSyncImages()); 6 | } 7 | 8 | async function onSyncImages() { 9 | const cache = await caches.open("contentIndex"); 10 | for (const { id, icons } of await index.getAll()) { 11 | for (const { src } of icons) { 12 | console.log(`Updating icon "${src}" for ${id}`); 13 | await cache.put( 14 | src, 15 | await fetch(src) 16 | ); 17 | } 18 | } 19 | } 20 | }); 21 | 22 | await periodicSync.register("images", { 23 | minInterval: 5 * 60 * 1000 // Refresh every 5 minutes 24 | }); 25 | 26 | // Ran elsewhere by scheduler 27 | // Is usually managed by generatePeriodicSyncVirtualEvents 28 | await dispatchEvent({ 29 | type: "periodicsync", 30 | tag: "images", 31 | schedule: { 32 | immediate: true 33 | // can give delay here or cron 34 | // minInterval doesn't always mean a fixed rate 35 | // 36 | // delay: 5 * 60 * 1000, 37 | // repeat: true 38 | } 39 | }); -------------------------------------------------------------------------------- /src/data/partner/schema.ts: -------------------------------------------------------------------------------- 1 | import { organisationBaseData } from "../organisation/schema"; 2 | 3 | export const partnerData = { 4 | type: "object", 5 | properties: { 6 | ...organisationBaseData.properties, 7 | partnerName: { 8 | type: "string", 9 | }, 10 | }, 11 | required: ["partnerName"], 12 | } as const; 13 | 14 | export const partner = { 15 | type: "object", 16 | properties: { 17 | partnerId: { 18 | type: "string", 19 | }, 20 | organisationId: { 21 | type: "string", 22 | }, 23 | partnerName: { 24 | type: "string", 25 | }, 26 | countryCode: { 27 | type: "string", 28 | nullable: true, 29 | }, 30 | accessToken: { 31 | type: "string", 32 | nullable: true, 33 | }, 34 | createdAt: { 35 | type: "string", 36 | }, 37 | updatedAt: { 38 | type: "string", 39 | }, 40 | approved: { 41 | type: "boolean", 42 | nullable: true, 43 | }, 44 | approvedAt: { 45 | type: "string", 46 | nullable: true, 47 | }, 48 | approvedByUserId: { 49 | type: "string", 50 | nullable: true, 51 | }, 52 | }, 53 | required: [ 54 | "partnerName", 55 | "organisationId", 56 | "partnerId", 57 | "createdAt", 58 | "updatedAt", 59 | ], 60 | } as const; 61 | -------------------------------------------------------------------------------- /src/tests/worker/service-worker/configured/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "./esnext/tests/worker/service-worker/configured/config.json", 3 | "services": [ 4 | { 5 | "name": "prices", 6 | "url": "./prices.js" 7 | }, 8 | { 9 | "name": "products", 10 | "url": "./products.js" 11 | }, 12 | { 13 | "name": "offers", 14 | "url": "./offers.js", 15 | "bindings": [ 16 | { 17 | "protocol": "products", 18 | "service": "products" 19 | }, 20 | { 21 | "protocol": "prices", 22 | "json": [ 23 | { 24 | "productId": "apples", 25 | "value": 2 26 | }, 27 | { 28 | "productId": "pears", 29 | "value": 2.5 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | ], 36 | "sockets": [ 37 | { 38 | "service": "offers", 39 | "address": "*:3000" 40 | } 41 | ], 42 | "extensions": [ 43 | "./extension.js" 44 | ] 45 | } -------------------------------------------------------------------------------- /src/data/authentication-state/set-authentication-state.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticationState, AuthenticationStateData } from "./types"; 2 | import { 3 | DEFAULT_AUTHENTICATION_STATE_EXPIRES_MS, 4 | getAuthenticationStateStore, 5 | } from "./store"; 6 | import { getExpiresAt } from "../expiring-kv"; 7 | import {v4} from "uuid"; 8 | import {getMaybePartner, getMaybeUser} from "../../authentication"; 9 | 10 | export async function setAuthenticationState( 11 | data: AuthenticationStateData & Partial 12 | ) { 13 | const stateId = data.stateId || v4(); 14 | const stateKey = stateId; 15 | const createdAt = data.createdAt || new Date().toISOString(); 16 | let createdBy: Partial = {}; 17 | if (!(data.createdAt || data.createdByUserId || data.createdByOrganisationId)) { 18 | createdBy = { 19 | createdByUserId: getMaybeUser()?.userId, 20 | createdByOrganisationId: getMaybePartner()?.organisationId 21 | } 22 | } 23 | const state: AuthenticationState = { 24 | createdAt, 25 | ...data, 26 | ...createdBy, 27 | stateId, 28 | stateKey, 29 | expiresAt: getExpiresAt( 30 | DEFAULT_AUTHENTICATION_STATE_EXPIRES_MS, 31 | data.expiresAt 32 | ), 33 | }; 34 | const store = getAuthenticationStateStore(); 35 | await store.set(stateId, state); 36 | return state; 37 | } 38 | -------------------------------------------------------------------------------- /src/tests/cache.ts: -------------------------------------------------------------------------------- 1 | import {caches} from "../fetch"; 2 | import {ok} from "../is"; 3 | import {dispatchEvent} from "../events"; 4 | import {v4} from "uuid"; 5 | 6 | { 7 | const name = v4(); 8 | const cache = await caches.open(name); 9 | 10 | let response; 11 | if (!(response = await cache.match("https://example.com"))) { 12 | response = await fetch("https://example.com"); 13 | await cache.put(response.url, response); 14 | } 15 | ok(await response.text()); 16 | const match = await cache.match("https://example.com"); 17 | ok(match); 18 | ok(await match.text()); 19 | 20 | await caches.delete(name) 21 | } 22 | 23 | { 24 | const name = v4(); 25 | const cache = await caches.open(name); 26 | const url = "https://example.com"; 27 | 28 | const initial = await cache.match(url); 29 | 30 | ok(!initial); 31 | 32 | await dispatchEvent({ 33 | type: "fetch", 34 | schedule: { 35 | immediate: true 36 | }, 37 | request: { 38 | url 39 | }, 40 | cache: { 41 | name 42 | } 43 | }); 44 | 45 | const match = await cache.match(url); 46 | 47 | ok(match); 48 | ok(match.ok); 49 | 50 | await caches.delete(name); 51 | 52 | const deleted = await cache.match(url); 53 | 54 | ok(!deleted); 55 | } -------------------------------------------------------------------------------- /src/tests/worker/service-worker/store/register.ts: -------------------------------------------------------------------------------- 1 | import {DurableServiceWorkerRegistration, FetchFn, serviceWorker} from "../../../../worker"; 2 | import {fileURLToPath} from "node:url"; 3 | import {dirname, join} from "node:path"; 4 | import {FetchStore} from "../fetch-store"; 5 | 6 | let registration: DurableServiceWorkerRegistration | undefined = undefined; 7 | 8 | export const fetch: FetchFn = async (input, init) => { 9 | if (!registration) { 10 | const pathname = fileURLToPath(import.meta.url); 11 | const path = join(dirname(pathname), "./store.js"); 12 | registration = await serviceWorker.register(path) 13 | } 14 | return registration.fetch(input, init) 15 | } 16 | 17 | export const json = new FetchStore({ 18 | type: "json", 19 | headers: { 20 | "Content-Type": "application/json" 21 | }, 22 | body: JSON.stringify, 23 | fetch 24 | }) 25 | 26 | export const text = new FetchStore({ 27 | type: "text", 28 | headers: { 29 | "Content-Type": "text/plain" 30 | }, 31 | fetch 32 | }) 33 | 34 | export const formData = new FetchStore({ 35 | type: "formData", 36 | fetch 37 | }) 38 | 39 | export const blob = new FetchStore({ 40 | type: "blob", 41 | fetch 42 | }) 43 | 44 | export const arrayBuffer = new FetchStore({ 45 | type: "arrayBuffer", 46 | fetch 47 | }) -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PROMPT_NAME=test name 2 | PROMPT_EMAIL=test+email@example.com 3 | OTEL_SERVICE_NAME=discord 4 | HONEYCOMB_API_KEY= 5 | ENABLE_SEED=true 6 | DISCORD_CLIENT_ID= 7 | DISCORD_CLIENT_SECRET= 8 | DISCORD_INVITE_URL= 9 | DISCORD_REDIRECT_URL=http://localhost:3000/api/authentication/discord/callback 10 | DISCORD_BOT_TOKEN= 11 | DISCORD_BOT_PERMISSIONS=65983381777601 12 | DISCORD_SERVER_ID= 13 | DEFAULT_TIMEZONE=Pacific/Auckland 14 | IS_LOCAL= 15 | WEBAUTHN_RP_NAME= 16 | WEBAUTHN_RP_ID= 17 | WEBAUTHN_RP_ORIGIN= 18 | AUTHSIGNAL_TENANT= 19 | AUTHSIGNAL_KEY= 20 | AUTHSIGNAL_SECRET= 21 | AUTHSIGNAL_API_URL= 22 | AUTHSIGNAL_REDIRECT_URL= 23 | AUTHSIGNAL_WEBAUTHN= 24 | MEDIA_PREFER_FIXED_ORDER=1 25 | ALLOW_ANONYMOUS_USER= 26 | LISTEN_HOST= 27 | INVITEE_ORIGIN= 28 | PUBLIC_PRODUCTS=1 29 | PUBLIC_SERVICES=1 30 | GENERIC_PRODUCTS=1 31 | GENERIC_SERVICES=1 32 | COOKIE_SECRET=example 33 | MAIL_MAILER=smtp 34 | MAIL_HOST= 35 | MAIL_PORT="587" 36 | MAIL_USERNAME= 37 | MAIL_PASSWORD= 38 | MAIL_ENCRYPTION="tls" 39 | MAIL_FROM=noreply@example.com 40 | MAIL_SENDER= 41 | MAIL_REPLY_TO=help@example.com 42 | QSTASH_URL=https://qstash.upstash.io/v1/publish/ 43 | QSTASH_MESSAGES_URL=https://qstash.upstash.io/v1/messages/ 44 | QSTASH_EVENT_URL=/api/event 45 | QSTASH_TOKEN= 46 | QSTASH_CURRENT_SIGNING_KEY= 47 | QSTASH_NEXT_SIGNING_KEY= 48 | INTERNAL_KEY_SECRET=example 49 | KV_CONNECT_URL=kv.db -------------------------------------------------------------------------------- /src/listen/task/get-task.ts: -------------------------------------------------------------------------------- 1 | import {FastifyInstance} from "fastify"; 2 | import { getTask, taskSchema } from "../../data"; 3 | import { authenticate } from "../authentication"; 4 | import {isUnauthenticated} from "../../authentication"; 5 | 6 | export async function getTaskRoutes(fastify: FastifyInstance) { 7 | const params = { 8 | type: "object", 9 | properties: { 10 | taskId: { 11 | type: "string", 12 | }, 13 | }, 14 | required: ["taskId"], 15 | }; 16 | 17 | const response = { 18 | 200: { 19 | description: "A task", 20 | ...taskSchema.task, 21 | }, 22 | }; 23 | 24 | const schema = { 25 | description: "Get a task", 26 | tags: ["task"], 27 | summary: "", 28 | response, 29 | params, 30 | security: [ 31 | { 32 | apiKey: [] as string[], 33 | }, 34 | ], 35 | }; 36 | 37 | type Schema = { 38 | Params: { 39 | taskId: string; 40 | }; 41 | }; 42 | 43 | try { 44 | fastify.get("/:taskId", { 45 | schema, 46 | preHandler: authenticate(fastify), 47 | async handler(request, response) { 48 | const task = await getTask(request.params.taskId); 49 | if (!task || (isUnauthenticated() || !task.public)) response.status(404); 50 | response.send(task); 51 | }, 52 | }); 53 | } catch {} 54 | } 55 | -------------------------------------------------------------------------------- /src/listen/auth/anonymous.ts: -------------------------------------------------------------------------------- 1 | import {FastifyInstance} from "fastify"; 2 | import {logoutResponse} from "./logout"; 3 | import {addAnonymousCookieState, getAuthenticationState, getExchangeStateURL, getInviteURL} from "../../data"; 4 | import {setAuthenticationStateCookie} from "../authentication"; 5 | import {getOrigin} from "../config"; 6 | 7 | export async function anonymousRoutes(fastify: FastifyInstance) { 8 | 9 | const { 10 | ALLOW_ANONYMOUS_USER 11 | } = process.env; 12 | 13 | if (!ALLOW_ANONYMOUS_USER) return; 14 | 15 | type Querystring = { 16 | redirect?: string; 17 | state?: string; 18 | } 19 | type Schema = { 20 | Querystring: Querystring 21 | }; 22 | 23 | fastify.get("/anonymous", { 24 | async handler(request, response) { 25 | await logoutResponse(response); 26 | 27 | const state = await addAnonymousCookieState(); 28 | setAuthenticationStateCookie(response, state); 29 | 30 | const { state: userState, redirect } = request.query; 31 | 32 | const exchange = await getExchangeStateURL(userState); 33 | const location = exchange || redirect || "/home"; 34 | 35 | response.header("Location", location); 36 | response.status(302); 37 | response.send(); 38 | 39 | } 40 | }) 41 | } --------------------------------------------------------------------------------