From 39f1e16bfb929c6078aa37822a72d700b9473747 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Fri, 3 Jan 2025 17:48:37 -0800 Subject: [PATCH] Durables --- .vscode/settings.json | 5 + .../neon/oauth/idp/[[...route]]/idp.ts | 4 +- packages/durables/.eslintrc.cjs | 6 + packages/durables/.gitignore | 4 + packages/durables/CHANGELOG.md | 1224 +++++++++++++++++ packages/durables/LICENSE | 7 + packages/durables/README.md | 26 + packages/durables/examples/table.tsx | 30 + packages/durables/examples/variable.tsx | 17 + packages/durables/package.json | 46 + packages/durables/src/durables/durable.tsx | 124 ++ packages/durables/src/durables/map.tsx | 26 + packages/durables/src/durables/table.tsx | 47 + packages/durables/src/durables/variable.tsx | 50 + packages/durables/src/errors.tsx | 13 + packages/durables/src/index.tsx | 5 + packages/durables/src/serialize.tsx | 1 + packages/durables/src/sql.tsx | 304 ++++ packages/durables/src/util/brands.tsx | 1 + packages/durables/src/util/hashes.tsx | 12 + packages/durables/tsconfig.json | 20 + packages/durables/tsup.config.ts | 37 + packages/stack-shared/src/utils/arrays.tsx | 4 + packages/stack-shared/src/utils/hashes.tsx | 12 +- packages/stack-shared/src/utils/strings.tsx | 23 +- packages/stack-shared/src/utils/urls.tsx | 11 + pnpm-lock.yaml | 24 + 27 files changed, 2075 insertions(+), 8 deletions(-) create mode 100644 packages/durables/.eslintrc.cjs create mode 100644 packages/durables/.gitignore create mode 100644 packages/durables/CHANGELOG.md create mode 100644 packages/durables/LICENSE create mode 100644 packages/durables/README.md create mode 100644 packages/durables/examples/table.tsx create mode 100644 packages/durables/examples/variable.tsx create mode 100644 packages/durables/package.json create mode 100644 packages/durables/src/durables/durable.tsx create mode 100644 packages/durables/src/durables/map.tsx create mode 100644 packages/durables/src/durables/table.tsx create mode 100644 packages/durables/src/durables/variable.tsx create mode 100644 packages/durables/src/errors.tsx create mode 100644 packages/durables/src/index.tsx create mode 100644 packages/durables/src/serialize.tsx create mode 100644 packages/durables/src/sql.tsx create mode 100644 packages/durables/src/util/brands.tsx create mode 100644 packages/durables/src/util/hashes.tsx create mode 100644 packages/durables/tsconfig.json create mode 100644 packages/durables/tsup.config.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index e47a7e50a0..9c15f5e621 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "editor.tabSize": 2, "cSpell.words": [ "backlinks", + "BIGSERIAL", "Cdfc", "cjsx", "clsx", @@ -22,6 +23,7 @@ "EDNS", "EMESSAGE", "Falsey", + "fkey", "frontends", "geoip", "hookform", @@ -49,6 +51,7 @@ "otlp", "pageleave", "pageview", + "pglite", "pkcco", "PKCE", "pooler", @@ -63,6 +66,8 @@ "RPID", "simplewebauthn", "spoofable", + "Sqlizable", + "sqlize", "stackauth", "stackframe", "supabase", diff --git a/apps/backend/src/app/api/v1/integrations/neon/oauth/idp/[[...route]]/idp.ts b/apps/backend/src/app/api/v1/integrations/neon/oauth/idp/[[...route]]/idp.ts index 9732657f0e..91ef10c6fc 100644 --- a/apps/backend/src/app/api/v1/integrations/neon/oauth/idp/[[...route]]/idp.ts +++ b/apps/backend/src/app/api/v1/integrations/neon/oauth/idp/[[...route]]/idp.ts @@ -3,7 +3,7 @@ import { Prisma } from '@prisma/client'; import { decodeBase64OrBase64Url } from '@stackframe/stack-shared/dist/utils/bytes'; import { getEnvVariable } from '@stackframe/stack-shared/dist/utils/env'; import { StackAssertionError, captureError, throwErr } from '@stackframe/stack-shared/dist/utils/errors'; -import { sha512 } from '@stackframe/stack-shared/dist/utils/hashes'; +import { sha512ToHex } from '@stackframe/stack-shared/dist/utils/hashes'; import { getPerAudienceSecret, getPrivateJwk, getPublicJwkSet } from '@stackframe/stack-shared/dist/utils/jwt'; import { deindent } from '@stackframe/stack-shared/dist/utils/strings'; import { generateUuid } from '@stackframe/stack-shared/dist/utils/uuids'; @@ -186,7 +186,7 @@ export async function createOidcProvider(options: { id: string, baseUrl: string ttl: {}, cookies: { keys: [ - await sha512(`oidc-idp-cookie-encryption-key:${getEnvVariable("STACK_SERVER_SECRET")}`), + await sha512ToHex(`oidc-idp-cookie-encryption-key:${getEnvVariable("STACK_SERVER_SECRET")}`), ], }, jwks: privateJwks, diff --git a/packages/durables/.eslintrc.cjs b/packages/durables/.eslintrc.cjs new file mode 100644 index 0000000000..9602974652 --- /dev/null +++ b/packages/durables/.eslintrc.cjs @@ -0,0 +1,6 @@ +module.exports = { + "extends": [ + "../../eslint-configs/defaults.js", + ], + "ignorePatterns": ['/*', '!/src', '!/examples'], +}; diff --git a/packages/durables/.gitignore b/packages/durables/.gitignore new file mode 100644 index 0000000000..2f52780b56 --- /dev/null +++ b/packages/durables/.gitignore @@ -0,0 +1,4 @@ +# we need to not ignore the next-env.d.ts file because it is not automatically recreated (we never run `next build` in this repo directly because Next is just a peer dependency) +!next-env.d.ts + +quetzal-translations diff --git a/packages/durables/CHANGELOG.md b/packages/durables/CHANGELOG.md new file mode 100644 index 0000000000..3282f5c041 --- /dev/null +++ b/packages/durables/CHANGELOG.md @@ -0,0 +1,1224 @@ +# @stackframe/stack + +## 2.7.0 + +### Minor Changes + +- Various changes + +### Patch Changes + +- Updated dependencies + - @stackframe/stack-shared@2.7.0 + - @stackframe/stack-ui@2.7.0 + - @stackframe/stack-sc@2.7.0 + +## 2.6.39 + +### Patch Changes + +- Updated dependencies + - @stackframe/stack-shared@2.6.39 + - @stackframe/stack-ui@2.6.39 + - @stackframe/stack-sc@2.6.39 + +## 2.6.38 + +### Patch Changes + +- Updated dependencies + - @stackframe/stack-shared@2.6.38 + - @stackframe/stack-ui@2.6.38 + - @stackframe/stack-sc@2.6.38 + +## 2.6.37 + +### Patch Changes + +- Updated dependencies + - @stackframe/stack-shared@2.6.37 + - @stackframe/stack-ui@2.6.37 + - @stackframe/stack-sc@2.6.37 + +## 2.6.36 + +### Patch Changes + +- Various updates +- Updated dependencies + - @stackframe/stack-shared@2.6.36 + - @stackframe/stack-ui@2.6.36 + - @stackframe/stack-sc@2.6.36 + +## 2.6.35 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.6.35 + - @stackframe/stack-ui@2.6.35 + - @stackframe/stack-sc@2.6.35 + +## 2.6.34 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.6.34 + - @stackframe/stack-ui@2.6.34 + - @stackframe/stack-sc@2.6.34 + +## 2.6.33 + +### Patch Changes + +- @stackframe/stack-sc@2.6.33 +- @stackframe/stack-shared@2.6.33 +- @stackframe/stack-ui@2.6.33 + +## 2.6.32 + +### Patch Changes + +- Updated dependencies + - @stackframe/stack-shared@2.6.32 + - @stackframe/stack-ui@2.6.32 + - @stackframe/stack-sc@2.6.32 + +## 2.6.31 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.6.31 + - @stackframe/stack-ui@2.6.31 + - @stackframe/stack-sc@2.6.31 + +## 2.6.30 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.6.30 + - @stackframe/stack-ui@2.6.30 + - @stackframe/stack-sc@2.6.30 + +## 2.6.29 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.6.29 + - @stackframe/stack-ui@2.6.29 + - @stackframe/stack-sc@2.6.29 + +## 2.6.28 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.6.28 + - @stackframe/stack-ui@2.6.28 + - @stackframe/stack-sc@2.6.28 + +## 2.6.27 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.6.27 + - @stackframe/stack-ui@2.6.27 + - @stackframe/stack-sc@2.6.27 + +## 2.6.26 + +### Patch Changes + +- Various bugfixes +- Updated dependencies + - @stackframe/stack-sc@2.6.26 + - @stackframe/stack-shared@2.6.26 + - @stackframe/stack-ui@2.6.26 + +## 2.6.25 + +### Patch Changes + +- Translation overrides +- Updated dependencies + - @stackframe/stack-shared@2.6.25 + - @stackframe/stack-ui@2.6.25 + - @stackframe/stack-sc@2.6.25 + +## 2.6.24 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.6.24 + - @stackframe/stack-ui@2.6.24 + - @stackframe/stack-sc@2.6.24 + +## 2.6.23 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.6.23 + - @stackframe/stack-sc@2.6.23 + - @stackframe/stack-ui@2.6.23 + +## 2.6.22 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.6.22 + - @stackframe/stack-sc@2.6.22 + - @stackframe/stack-ui@2.6.22 + +## 2.6.21 + +### Patch Changes + +- Fixed inviteUser +- Updated dependencies + - @stackframe/stack-shared@2.6.21 + - @stackframe/stack-sc@2.6.21 + - @stackframe/stack-ui@2.6.21 + +## 2.6.20 + +### Patch Changes + +- Next.js 15 fixes +- Updated dependencies + - @stackframe/stack-shared@2.6.20 + - @stackframe/stack-sc@2.6.20 + - @stackframe/stack-ui@2.6.20 + +## 2.6.19 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.6.19 + - @stackframe/stack-sc@2.6.19 + - @stackframe/stack-ui@2.6.19 + +## 2.6.18 + +### Patch Changes + +- fixed user update bug +- Updated dependencies + - @stackframe/stack-shared@2.6.18 + - @stackframe/stack-sc@2.6.18 + - @stackframe/stack-ui@2.6.18 + +## 2.6.17 + +### Patch Changes + +- Loading skeletons +- Updated dependencies + - @stackframe/stack-shared@2.6.17 + - @stackframe/stack-sc@2.6.17 + - @stackframe/stack-ui@2.6.17 + +## 2.6.16 + +### Patch Changes + +- - list user pagination + - fixed visual glitches +- Updated dependencies + - @stackframe/stack-shared@2.6.16 + - @stackframe/stack-ui@2.6.16 + - @stackframe/stack-sc@2.6.16 + +## 2.6.15 + +### Patch Changes + +- Passkeys +- Updated dependencies + - @stackframe/stack-shared@2.6.15 + - @stackframe/stack-ui@2.6.15 + - @stackframe/stack-sc@2.6.15 + +## 2.6.14 + +### Patch Changes + +- @stackframe/stack-sc@2.6.14 +- @stackframe/stack-shared@2.6.14 +- @stackframe/stack-ui@2.6.14 + +## 2.6.13 + +### Patch Changes + +- Updated docs +- Updated dependencies + - @stackframe/stack-shared@2.6.13 + - @stackframe/stack-ui@2.6.13 + - @stackframe/stack-sc@2.6.13 + +## 2.6.12 + +### Patch Changes + +- Updated account settings page +- Updated dependencies + - @stackframe/stack-shared@2.6.12 + - @stackframe/stack-sc@2.6.12 + - @stackframe/stack-ui@2.6.12 + +## 2.6.11 + +### Patch Changes + +- fixed account settings bugs +- Updated dependencies + - @stackframe/stack-shared@2.6.11 + - @stackframe/stack-sc@2.6.11 + - @stackframe/stack-ui@2.6.11 + +## 2.6.10 + +### Patch Changes + +- Various bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.6.10 + - @stackframe/stack-sc@2.6.10 + - @stackframe/stack-ui@2.6.10 + +## 2.6.9 + +### Patch Changes + +- - New contact channel API + - Fixed some visual gitches and typos + - Bug fixes +- Updated dependencies + - @stackframe/stack-shared@2.6.9 + - @stackframe/stack-sc@2.6.9 + - @stackframe/stack-ui@2.6.9 + +## 2.6.8 + +### Patch Changes + +- Updated dependencies + - @stackframe/stack-shared@2.6.8 + - @stackframe/stack-ui@2.6.8 + - @stackframe/stack-sc@2.6.8 + +## 2.6.7 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-sc@2.6.7 + - @stackframe/stack-shared@2.6.7 + - @stackframe/stack-ui@2.6.7 + +## 2.6.6 + +### Patch Changes + +- Bugfixes + - @stackframe/stack-sc@2.6.6 + - @stackframe/stack-shared@2.6.6 + - @stackframe/stack-ui@2.6.6 + +## 2.6.5 + +### Patch Changes + +- Minor improvements +- Updated dependencies + - @stackframe/stack-shared@2.6.5 + - @stackframe/stack-sc@2.6.5 + - @stackframe/stack-ui@2.6.5 + +## 2.6.4 + +### Patch Changes + +- fixed small problems +- Updated dependencies + - @stackframe/stack-shared@2.6.4 + - @stackframe/stack-sc@2.6.4 + - @stackframe/stack-ui@2.6.4 + +## 2.6.3 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.6.3 + - @stackframe/stack-ui@2.6.3 + - @stackframe/stack-sc@2.6.3 + +## 2.6.2 + +### Patch Changes + +- Several bugfixes & typos +- Updated dependencies + - @stackframe/stack-shared@2.6.2 + - @stackframe/stack-ui@2.6.2 + - @stackframe/stack-sc@2.6.2 + +## 2.6.1 + +### Patch Changes + +- Bugfixes + - @stackframe/stack-sc@2.6.1 + - @stackframe/stack-shared@2.6.1 + - @stackframe/stack-ui@2.6.1 + +## 2.6.0 + +### Minor Changes + +- OTP login, more providers, and styling improvements + +### Patch Changes + +- Updated dependencies + - @stackframe/stack-shared@2.6.0 + - @stackframe/stack-sc@2.6.0 + - @stackframe/stack-ui@2.6.0 + +## 2.5.37 + +### Patch Changes + +- client side account deletion; new account setting style; +- Updated dependencies + - @stackframe/stack-shared@2.5.37 + - @stackframe/stack-ui@2.5.37 + - @stackframe/stack-sc@2.5.37 + +## 2.5.36 + +### Patch Changes + +- added apple oauth +- Updated dependencies + - @stackframe/stack-shared@2.5.36 + - @stackframe/stack-sc@2.5.36 + - @stackframe/stack-ui@2.5.36 + +## 2.5.35 + +### Patch Changes + +- Doc improvements +- Updated dependencies + - @stackframe/stack-shared@2.5.35 + - @stackframe/stack-ui@2.5.35 + - @stackframe/stack-sc@2.5.35 + +## 2.5.34 + +### Patch Changes + +- Internationalization +- Updated dependencies + - @stackframe/stack-shared@2.5.34 + - @stackframe/stack-ui@2.5.34 + - @stackframe/stack-sc@2.5.34 + +## 2.5.33 + +### Patch Changes + +- Team membership webhooks +- Updated dependencies + - @stackframe/stack-shared@2.5.33 + - @stackframe/stack-ui@2.5.33 + - @stackframe/stack-sc@2.5.33 + +## 2.5.32 + +### Patch Changes + +- Updated dependencies + - @stackframe/stack-shared@2.5.32 + - @stackframe/stack-ui@2.5.32 + - @stackframe/stack-sc@2.5.32 + +## 2.5.31 + +### Patch Changes + +- JWKS +- Updated dependencies + - @stackframe/stack-shared@2.5.31 + - @stackframe/stack-ui@2.5.31 + - @stackframe/stack-sc@2.5.31 + +## 2.5.30 + +### Patch Changes + +- More OAuth providers +- Updated dependencies + - @stackframe/stack-shared@2.5.30 + - @stackframe/stack-ui@2.5.30 + - @stackframe/stack-sc@2.5.30 + +## 2.5.29 + +### Patch Changes + +- @stackframe/stack-sc@2.5.29 +- @stackframe/stack-shared@2.5.29 +- @stackframe/stack-ui@2.5.29 + +## 2.5.28 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.5.28 + - @stackframe/stack-ui@2.5.28 + - @stackframe/stack-sc@2.5.28 + +## 2.5.27 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.5.27 + - @stackframe/stack-sc@2.5.27 + - @stackframe/stack-ui@2.5.27 + +## 2.5.26 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-sc@2.5.26 + - @stackframe/stack-shared@2.5.26 + - @stackframe/stack-ui@2.5.26 + +## 2.5.25 + +### Patch Changes + +- GitLab OAuth provider +- Updated dependencies + - @stackframe/stack-shared@2.5.25 + - @stackframe/stack-ui@2.5.25 + - @stackframe/stack-sc@2.5.25 + +## 2.5.24 + +### Patch Changes + +- Various bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.5.24 + - @stackframe/stack-ui@2.5.24 + - @stackframe/stack-sc@2.5.24 + +## 2.5.23 + +### Patch Changes + +- Various bugfixes and performance improvements +- Updated dependencies + - @stackframe/stack-ui@2.5.23 + - @stackframe/stack-sc@2.5.23 + - @stackframe/stack-shared@2.5.23 + +## 2.5.22 + +### Patch Changes + +- Team metadata +- Updated dependencies + - @stackframe/stack-shared@2.5.22 + - @stackframe/stack-ui@2.5.22 + - @stackframe/stack-sc@2.5.22 + +## 2.5.21 + +### Patch Changes + +- Discord OAuth provider +- Updated dependencies + - @stackframe/stack-shared@2.5.21 + - @stackframe/stack-ui@2.5.21 + - @stackframe/stack-sc@2.5.21 + +## 2.5.20 + +### Patch Changes + +- Improved account settings +- Updated dependencies + - @stackframe/stack-shared@2.5.20 + - @stackframe/stack-ui@2.5.20 + - @stackframe/stack-sc@2.5.20 + +## 2.5.19 + +### Patch Changes + +- Team frontend components +- Updated dependencies + - @stackframe/stack-sc@2.5.19 + - @stackframe/stack-shared@2.5.19 + - @stackframe/stack-ui@2.5.19 + +## 2.5.18 + +### Patch Changes + +- Multi-factor authentication +- Updated dependencies + - @stackframe/stack-shared@2.5.18 + - @stackframe/stack-ui@2.5.18 + - @stackframe/stack-sc@2.5.18 + +## 2.5.17 + +### Patch Changes + +- Updated dependencies + - @stackframe/stack-shared@2.5.17 + - @stackframe/stack-ui@2.5.17 + - @stackframe/stack-sc@2.5.17 + +## 2.5.16 + +### Patch Changes + +- @stackframe/stack-sc@2.5.16 +- @stackframe/stack-shared@2.5.16 +- @stackframe/stack-ui@2.5.16 + +## 2.5.15 + +### Patch Changes + +- Webhooks +- Updated dependencies + - @stackframe/stack-shared@2.5.15 + - @stackframe/stack-sc@2.5.15 + - @stackframe/stack-ui@2.5.15 + +## 2.5.14 + +### Patch Changes + +- @stackframe/stack-sc@2.5.14 +- @stackframe/stack-shared@2.5.14 +- @stackframe/stack-ui@2.5.14 + +## 2.5.13 + +### Patch Changes + +- Add server side get connected account +- Updated dependencies + - @stackframe/stack-shared@2.5.13 + - @stackframe/stack-ui@2.5.13 + - @stackframe/stack-sc@2.5.13 + +## 2.5.12 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.5.12 + - @stackframe/stack-ui@2.5.12 + - @stackframe/stack-sc@2.5.12 + +## 2.5.11 + +### Patch Changes + +- Update descriptions & docs + - @stackframe/stack-sc@2.5.11 + - @stackframe/stack-shared@2.5.11 + - @stackframe/stack-ui@2.5.11 + +## 2.5.10 + +### Patch Changes + +- Facebook Business support +- Updated dependencies + - @stackframe/stack-shared@2.5.10 + - @stackframe/stack-ui@2.5.10 + - @stackframe/stack-sc@2.5.10 + +## 2.5.9 + +### Patch Changes + +- Impersonation +- Updated dependencies + - @stackframe/stack-shared@2.5.9 + - @stackframe/stack-ui@2.5.9 + - @stackframe/stack-sc@2.5.9 + +## 2.5.8 + +### Patch Changes + +- Improved docs +- Updated dependencies + - @stackframe/stack-shared@2.5.8 + - @stackframe/stack-ui@2.5.8 + - @stackframe/stack-sc@2.5.8 + +## 2.5.7 + +### Patch Changes + +- Bugfixes + - @stackframe/stack-sc@2.5.7 + - @stackframe/stack-shared@2.5.7 + - @stackframe/stack-ui@2.5.7 + +## 2.5.6 + +### Patch Changes + +- Updated dependencies + - @stackframe/stack-shared@2.5.6 + - @stackframe/stack-ui@2.5.6 + - @stackframe/stack-sc@2.5.6 + +## 2.5.5 + +### Patch Changes + +- @stackframe/stack-sc@2.5.5 +- @stackframe/stack-shared@2.5.5 +- @stackframe/stack-ui@2.5.5 + +## 2.5.4 + +### Patch Changes + +- Backend rework +- Updated dependencies + - @stackframe/stack-shared@2.5.4 + - @stackframe/stack-sc@2.5.4 + - @stackframe/stack-ui@2.5.4 + +## 2.5.3 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.5.3 + - @stackframe/stack-sc@2.5.3 + - @stackframe/stack-ui@2.5.3 + +## 2.5.2 + +### Patch Changes + +- Team profile pictures +- Updated dependencies + - @stackframe/stack-shared@2.5.2 + - @stackframe/stack-ui@2.5.2 + - @stackframe/stack-sc@2.5.2 + +## 2.5.1 + +### Patch Changes + +- New backend endpoints +- Updated dependencies + - @stackframe/stack-shared@2.5.1 + - @stackframe/stack-ui@2.5.1 + - @stackframe/stack-sc@2.5.1 + +## 2.5.0 + +### Minor Changes + +- Client teams and many bugfixes + +### Patch Changes + +- Updated dependencies + - @stackframe/stack-shared@2.5.0 + - @stackframe/stack-ui@2.5.0 + - @stackframe/stack-sc@2.5.0 + +## 2.4.28 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.4.28 + - @stackframe/stack-sc@2.4.28 + +## 2.4.27 + +### Patch Changes + +- Export MagicLinkSignIn component + - @stackframe/stack-sc@2.4.27 + - @stackframe/stack-shared@2.4.27 + +## 2.4.26 + +### Patch Changes + +- Improve docs +- Updated dependencies + - @stackframe/stack-shared@2.4.26 + - @stackframe/stack-sc@2.4.26 + +## 2.4.25 + +### Patch Changes + +- Docs update +- Updated dependencies + - @stackframe/stack-shared@2.4.25 + - @stackframe/stack-sc@2.4.25 + +## 2.4.24 + +### Patch Changes + +- Team switcher +- Updated dependencies + - @stackframe/stack-shared@2.4.24 + - @stackframe/stack-sc@2.4.24 + +## 2.4.23 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.4.23 + - @stackframe/stack-sc@2.4.23 + +## 2.4.22 + +### Patch Changes + +- OAuth scopes +- Updated dependencies + - @stackframe/stack-shared@2.4.22 + - @stackframe/stack-sc@2.4.22 + +## 2.4.21 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-sc@2.4.21 + - @stackframe/stack-shared@2.4.21 + +## 2.4.20 + +### Patch Changes + +- Support multiple projects on the same domain +- Updated dependencies + - @stackframe/stack-shared@2.4.20 + - @stackframe/stack-sc@2.4.20 + +## 2.4.19 + +### Patch Changes + +- Sync package versions +- Updated dependencies + - @stackframe/stack-sc@2.4.19 + - @stackframe/stack-shared@2.4.19 + +## 2.4.18 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.4.14 + +## 2.4.17 + +### Patch Changes + +- Bugfixes + +## 2.4.16 + +### Patch Changes + +- Improve setup flow +- Updated dependencies + - @stackframe/stack-shared@2.4.13 + - @stackframe/stack-sc@1.5.6 + +## 2.4.15 + +### Patch Changes + +- Improved client styling, added login form, added spotify oauth +- Updated dependencies + - @stackframe/stack-shared@2.4.12 + +## 2.4.14 + +### Patch Changes + +- Added email editor +- Updated dependencies + - @stackframe/stack-shared@2.4.11 + +## 2.4.13 + +### Patch Changes + +- Bug fixes +- Updated dependencies + - @stackframe/stack-shared@2.4.10 + - @stackframe/stack-sc@1.5.5 + +## 2.4.12 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.4.9 + - @stackframe/stack-sc@1.5.4 + +## 2.4.11 + +### Patch Changes + +- Cloudflare Workers support + +## 2.4.10 + +### Patch Changes + +- Updated dependencies + - @stackframe/stack-shared@2.4.8 + +## 2.4.9 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.4.7 + - @stackframe/stack-sc@1.5.3 + +## 2.4.8 + +### Patch Changes + +- Remove crypto-browserify dependency +- Updated dependencies + - @stackframe/stack-shared@2.4.6 + +## 2.4.7 + +### Patch Changes + +- Team selection +- Updated dependencies + - @stackframe/stack-shared@2.4.5 + +## 2.4.6 + +### Patch Changes + +- UX improvements +- Updated dependencies + - @stackframe/stack-shared@2.4.4 + +## 2.4.5 + +### Patch Changes + +- CRUD schemas +- Updated dependencies + - @stackframe/stack-shared@2.4.3 + - @stackframe/stack-sc@1.5.2 + +## 2.4.4 + +### Patch Changes + +- Theme updates + +## 2.4.3 + +### Patch Changes + +- New projects page +- Updated dependencies + - @stackframe/stack-shared@2.4.2 + +## 2.4.2 + +### Patch Changes + +- @stackframe package namespace + +## 2.4.1 + +### Patch Changes + +- Teams, permissions and RBAC +- Updated dependencies + - @stackframe/stack-shared@2.4.1 + - @stackframe/stack-sc@1.5.1 + +## 2.4.0 + +### Minor Changes + +- Middleware support + +### Patch Changes + +- Updated dependencies + - @stackframe/stack-shared@2.4.0 + - @stackframe/stack-sc@1.5.0 + +## 2.3.8 + +### Patch Changes + +- Bugfixes +- Updated dependencies + - @stackframe/stack-shared@2.3.6 + +## 2.3.7 + +### Patch Changes + +- CommonJS support +- Updated dependencies + - @stackframe/stack-shared@2.3.5 + +## 2.3.6 + +### Patch Changes + +- Updated dependencies + - @stackframe/stack-shared@2.3.4 + +## 2.3.5 + +### Patch Changes + +- Partial pre-rendering +- Updated dependencies + - @stackframe/stack-shared@2.3.3 + +## 2.3.4 + +### Patch Changes + +- E2E tests and various fixes + +## 2.3.3 + +### Patch Changes + +- Magic link configuration +- Updated dependencies + - @stackframe/stack-shared@2.3.2 + +## 2.3.2 + +### Patch Changes + +- Add README file +- Updated dependencies + - @stackframe/stack-shared@2.3.1 + - @stackframe/stack-sc@1.4.1 + +## 2.3.1 + +### Patch Changes + +- Improve e-mail validation and add request timeouts + +## 2.3.0 + +### Minor Changes + +- 96c26a7: added magic link + +### Patch Changes + +- Various small improvements +- Updated dependencies +- Updated dependencies [96c26a7] + - @stackframe/stack-shared@2.3.0 + - @stackframe/stack-sc@1.4.0 + +## 2.2.5 + +### Patch Changes + +- 5909ecd: fixed bugs +- Updated dependencies [5909ecd] + - @stackframe/stack-sc@1.3.2 + - @stackframe/stack-shared@2.2.2 + +## 2.2.4 + +### Patch Changes + +- fixed minor errors +- Updated dependencies + - @stackframe/stack-sc@1.3.1 + - @stackframe/stack-shared@2.2.1 + +## 2.2.3 + +### Patch Changes + +- fixed access token parsing error + +## 2.2.2 + +### Patch Changes + +- fixed mui import problem + +## 2.2.1 + +### Patch Changes + +- fixed dependency bug + +## 2.2.0 + +### Minor Changes + +- 2995d96: Added new UserButton component and Account setting page + +### Patch Changes + +- 2995d96: Fixed signin title bug +- Updated dependencies [2995d96] + - @stackframe/stack-shared@2.2.0 + +## 2.1.3 + +### Patch Changes + +- 4f985be: Fixed signin title bug + +## 2.1.2 + +### Patch Changes + +- 2eda71b: Fixed UI bugs + +## 2.1.1 + +### Patch Changes + +- fixed theme config bug + +## 2.1.0 + +### Minor Changes + +- 9e9907f: Added new stack UI system + +### Patch Changes + +- Updated dependencies [9e9907f] + - @stackframe/stack-shared@2.1.0 + - @stackframe/stack-sc@1.3.0 + +## 2.0.0 + +### Major Changes + +- 948252f: removed redirect URL in function options, redirect automatically to default URL + +### Patch Changes + +- Updated dependencies [948252f] + - @stackframe/stack-shared@2.0.0 + +## 1.2.1 + +### Patch Changes + +- fixed import bugs +- Updated dependencies + - @stackframe/stack-sc@1.2.1 + - @stackframe/stack-shared@1.2.1 + +## 1.2.0 + +### Minor Changes + +- Fixed infinite reload bug, added dashboard provider update + +### Patch Changes + +- Updated dependencies + - @stackframe/stack-sc@1.2.0 + - @stackframe/stack-shared@1.2.0 + +## 1.1.1 + +### Patch Changes + +- Updated dependencies + - @stackframe/stack-sc@1.1.1 + +## 1.1.0 + +### Minor Changes + +- fixed bugs + +### Patch Changes + +- Updated dependencies + - @stackframe/stack-shared@1.1.0 + - @stackframe/stack-sc@1.1.0 diff --git a/packages/durables/LICENSE b/packages/durables/LICENSE new file mode 100644 index 0000000000..0ec883d12e --- /dev/null +++ b/packages/durables/LICENSE @@ -0,0 +1,7 @@ +Copyright 2024 Stackframe + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/durables/README.md b/packages/durables/README.md new file mode 100644 index 0000000000..a10051145c --- /dev/null +++ b/packages/durables/README.md @@ -0,0 +1,26 @@ +# Stack Auth: Open-source Clerk/Auth0 alternative + +## [📘 Docs](https://docs.stack-auth.com) | [☁️ Hosted Version](https://stack-auth.com/) | [✨ Demo](https://demo.stack-auth.com/) | [🎮 Discord](https://discord.stack-auth.com) | [GitHub](https://github.com/stack-auth/stack) + +Stack Auth is a managed user authentication solution. It is developer-friendly and fully open-source (licensed under MIT and AGPL). + +Stack gets you started in just five minutes, after which you'll be ready to use all of its features as you grow your project. Our managed service is completely optional and you can export your user data and self-host, for free, at any time. + +We support Next.js frontends, along with any backend that can use our [REST API](https://docs.stack-auth.com/rest-api/overview). Check out our [setup guide](https://docs.stack-auth.com/getting-started/setup) to get started. + +## 📦 Installation & Setup + +1. Run Stack’s installation wizard with the following command: + ```bash + npx @stackframe/init-stack@latest + ``` +2. Then, create an account on the [Stack Auth dashboard](https://app.stack-auth.com/projects), create a new project with an API key, and copy its environment variables into the .env.local file of your Next.js project: + ``` + NEXT_PUBLIC_STACK_PROJECT_ID= + NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY= + STACK_SECRET_SERVER_KEY= + ``` +3. That's it! You can run your app with `npm run dev` and go to [http://localhost:3000/handler/signup](http://localhost:3000/handler/signup) to see the sign-up page. You can also check out the account settings page at [http://localhost:3000/handler/account-settings](http://localhost:3000/handler/account-settings). + + +Check out the [documentation](https://docs.stack-auth.com/getting-started/setup) for a more detailed guide. diff --git a/packages/durables/examples/table.tsx b/packages/durables/examples/table.tsx new file mode 100644 index 0000000000..e7f2f2c2b2 --- /dev/null +++ b/packages/durables/examples/table.tsx @@ -0,0 +1,30 @@ +// Ensure the following environment variable is set: +// DURABLES_CONNECTION_STRING=memory:// + +// NOTE: It is not usually advisable to use the DurableTable class directly. Instead, use one of the higher-level +// durable types, such as DurableMap or DurableBag, as they provide a more convenient API. + +import { DurableTable, query, sql } from "@stackframe/durables"; + +async function main() { + const table = new DurableTable({ + id: "durables-table-example:v1", + createTable: (tableName) => sql` + CREATE TABLE IF NOT EXISTS ${tableName} ( + id BIGSERIAL PRIMARY KEY, + value TEXT + ) + `, + }); + + const emptyTableResult = await query`SELECT id, value FROM ${table}`; + console.log("No rows:", emptyTableResult.rows); // prints [] + // (if we use a persistent store instead of memory, it may have some data from previous runs) + + const value = `It's ${new Date().toISOString()}`; + await query`INSERT INTO ${table} (value) VALUES (${value})`; + + const updatedTableResult = await query`SELECT id, value FROM ${table}`; + console.log("Inserted row:", updatedTableResult.rows); // prints [{ id: 'some-id', value: "It's " }] +} +main().catch(console.error); diff --git a/packages/durables/examples/variable.tsx b/packages/durables/examples/variable.tsx new file mode 100644 index 0000000000..1e662ceb55 --- /dev/null +++ b/packages/durables/examples/variable.tsx @@ -0,0 +1,17 @@ +// Ensure the following environment variable is set: +// DURABLES_CONNECTION_STRING=memory:// + +import { DurableVariable } from "@stackframe/durables"; + +async function main() { + const variable = new DurableVariable("durables-variable-example:v1"); + + + // You can get or set variables + console.log("Value:", await variable.get()); // prints undefined + await variable.set("Hello, world!"); + console.log("Updated value:", await variable.get()); // prints "Hello, world!" +} +main().catch(console.error); + + diff --git a/packages/durables/package.json b/packages/durables/package.json new file mode 100644 index 0000000000..bda829ab5f --- /dev/null +++ b/packages/durables/package.json @@ -0,0 +1,46 @@ +{ + "name": "@stackframe/durables", + "version": "2.7.0", + "sideEffects": false, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": { + "default": "./dist/esm/index.js" + }, + "require": { + "default": "./dist/index.js" + } + } + }, + "homepage": "https://stack-auth.com", + "scripts": { + "typecheck": "tsc --noEmit", + "build": "rimraf dist && pnpm run css && tsup-node", + "clean": "rimraf dist && rimraf node_modules", + "dev": "rimraf dist && tsup-node --watch", + "lint": "eslint --ext .tsx,.ts .", + "tsx": "DURABLES_CONNECTION_STRING=${DURABLES_CONNECTION_STRING:-memory://} tsx", + "example:table": "pnpm run tsx examples/table.tsx", + "example:variable": "pnpm run tsx examples/variable.tsx" + }, + "files": [ + "README.md", + "dist", + "CHANGELOG.md", + "LICENSE" + ], + "dependencies": { + "@stackframe/stack-shared": "workspace:*", + "@electric-sql/pglite": "^0.2.15" + }, + "peerDependencies": {}, + "peerDependenciesMeta": {}, + "devDependencies": { + "esbuild": "^0.20.2", + "tsup": "^8.0.2", + "rimraf": "^5.0.5" + } +} diff --git a/packages/durables/src/durables/durable.tsx b/packages/durables/src/durables/durable.tsx new file mode 100644 index 0000000000..c428f18752 --- /dev/null +++ b/packages/durables/src/durables/durable.tsx @@ -0,0 +1,124 @@ + +import { createDurablesError } from "../errors"; +import { SqlIdentifier, SqlPart, query, sql } from "../sql"; + +type DurableStringId = `${Lowercase}:v${number}`; +export type DurableIdInput = DurableStringId | [Durable, DurableStringId]; +export type DurableId = DurableIdInput & { + __brand: 'DurableId', +}; + +export const durableIdSymbol = Symbol.for('durables--durableId'); + +export type Durable = { + [durableIdSymbol]: DurableId, +}; + +export function assertDurableId(id: DurableIdInput): asserts id is DurableId { + if (typeof id !== 'string') { + throw createDurablesError` + Invalid ID: ${id}. Must be a string + `(); + } + if (id.length === 0) { + throw createDurablesError` + Invalid empty ID. Must be a non-empty string + `(); + } + + const splitByColon = id.split(':'); + if (splitByColon.length !== 2) { + throw createDurablesError` + Invalid ID: ${id}. Must follow the format: :v + `(); + } + + const name = splitByColon[0]; + const version = splitByColon[1]; + + if (!name.match(/^[a-z0-9-]+$/)) { + throw createDurablesError` + Invalid ID: ${id}. Name must be lowercase alphanumeric with dashes + `(); + } + + if (!version.match(/^v([1-9][0-9]*|0)$/)) { + throw createDurablesError` + Invalid ID: ${id}. Version must be a positive integer + `(); + } +} + +export function flattenId(id: DurableId): { allParentIds: string[], id: string } { + if (typeof id === 'string') { + return { allParentIds: [], id }; + } else { + const flattenedParent = flattenId(id[0][durableIdSymbol]); + return { + allParentIds: [...flattenedParent.allParentIds, flattenedParent.id], + id: id[1], + }; + } +} + + +// Even though we don't have to do this as the operations below are idempotent, we save ourselves a few queries +let _hasCreatedDurablesTable = false; +let _createdDurableIdsInDurablesTable: Set = new Set(); + +export async function getMainTableRowInfo(id: DurableId): Promise<{ tableName: SqlIdentifier, rowCondition: SqlPart, rowAllParentIds: string[], rowId: string }> { + const { allParentIds, id: durableId } = flattenId(id); + + if (!_hasCreatedDurablesTable) { + await query` + CREATE TABLE IF NOT EXISTS _durables ( + db_internal_id BIGSERIAL PRIMARY KEY, + db_internal_parent_id BIGINT, + id TEXT NOT NULL, + all_parent_ids TEXT[] NOT NULL, + value JSONB, + CONSTRAINT _durables_parent_id_fkey FOREIGN KEY (db_internal_parent_id) REFERENCES _durables(db_internal_id) ON DELETE CASCADE, + CONSTRAINT _durables_all_parent_ids_id_unique UNIQUE (all_parent_ids, id) + ) + `; + await query` + CREATE INDEX IF NOT EXISTS _durables_id_idx ON _durables (db_internal_id) + `; + await query` + CREATE INDEX IF NOT EXISTS _durables_parent_id_idx ON _durables (db_internal_parent_id) + `; + await query` + CREATE INDEX IF NOT EXISTS _durables_all_parent_ids_idx ON _durables USING GIN (all_parent_ids) + `; + await query` + CREATE INDEX IF NOT EXISTS _durables_all_parent_ids_id_idx ON _durables (all_parent_ids, id) + `; + _hasCreatedDurablesTable = true; + } + + if (!_createdDurableIdsInDurablesTable.has(durableId)) { + await query` + INSERT INTO _durables ( + id, + all_parent_ids, + db_internal_parent_id + ) + VALUES ( + ${durableId}, + ${allParentIds}, + ${allParentIds.length === 0 ? null : sql` + (SELECT db_internal_id FROM _durables WHERE all_parent_ids = ${allParentIds.slice(0, -1)} AND id = ${allParentIds[allParentIds.length - 1]}) + `} + ) + ON CONFLICT (all_parent_ids, id) DO NOTHING; + `; + _createdDurableIdsInDurablesTable.add(durableId); + } + + return { + tableName: sql.identifier('_durables'), + rowCondition: sql`all_parent_ids = ${allParentIds} AND id = ${durableId}`, + rowAllParentIds: allParentIds, + rowId: durableId, + }; +} diff --git a/packages/durables/src/durables/map.tsx b/packages/durables/src/durables/map.tsx new file mode 100644 index 0000000000..59441cb2e9 --- /dev/null +++ b/packages/durables/src/durables/map.tsx @@ -0,0 +1,26 @@ +export type AsyncMap = { + clear: () => Promise, + delete: (key: K) => Promise, + get: (key: K) => Promise, + has: (key: K) => Promise, + set: (key: K, value: V) => Promise, + entries: () => Promise>, + keys: () => Promise>, + values: () => Promise>, + [Symbol.asyncIterator]: () => AsyncIterableIterator<[K, V]>, +}; + +/* +export class DurableMap implements AsyncMap { + private readonly table: + + constructor(private readonly id: string) { + // nothing to do here + } + + async clear() { + await sql`DELETE FROM ${this.id}`; + } +} + +*/ diff --git a/packages/durables/src/durables/table.tsx b/packages/durables/src/durables/table.tsx new file mode 100644 index 0000000000..e0ad97bd5b --- /dev/null +++ b/packages/durables/src/durables/table.tsx @@ -0,0 +1,47 @@ +import { createDurablesError } from "../errors"; +import { Sql, SqlIdentifier, query, sql, toSqlPartSymbol } from "../sql"; +import { generateHashedCode } from "../util/hashes"; +import { Durable, DurableId, DurableIdInput, assertDurableId, durableIdSymbol, flattenId } from "./durable"; + + +export class DurableTable implements Durable { + private readonly _id: DurableId; + private readonly _createTablePromise: Promise<{ tableName: SqlIdentifier }>; + + constructor(options: { + id: DurableIdInput, + createTable: (tableName: SqlIdentifier) => Sql, + }) { + assertDurableId(options.id); + this._id = options.id; + const flattenedId = flattenId(this._id); + + this._createTablePromise = (async () => { + const dbTableNameHash = await generateHashedCode("DurableTable-table-name", JSON.stringify(flattenedId)); + const sqlIdentifier = sql.identifier(`_durables_${dbTableNameHash}`); + const createSql = options.createTable(sqlIdentifier).map(async (parts) => { + if (!['CREATE TABLE IF NOT EXISTS', '/* DURABLES-IGNORE-PREFIX-WARNING */'].some(prefix => parts.rawStrings[0].trim().toUpperCase().startsWith(prefix))) { + throw createDurablesError` + SQL returned from createTable must start with CREATE TABLE IF NOT EXISTS. + + In case this is intended, add the prefix '/* DURABLES-IGNORE-PREFIX-WARNING */' (without quotes) instead. + + Received: ${parts.rawStrings[0]} + `(); + } + return parts; + }); + await query(createSql); + return { tableName: sqlIdentifier }; + })(); + } + + get [durableIdSymbol]() { + return this._id; + } + + async [toSqlPartSymbol]() { + const { tableName } = await this._createTablePromise; + return tableName; + } +} diff --git a/packages/durables/src/durables/variable.tsx b/packages/durables/src/durables/variable.tsx new file mode 100644 index 0000000000..ce5d383bd0 --- /dev/null +++ b/packages/durables/src/durables/variable.tsx @@ -0,0 +1,50 @@ +import { query } from "../sql"; +import { Durable, DurableId, DurableIdInput, assertDurableId, durableIdSymbol, getMainTableRowInfo } from "./durable"; + + +export class DurableVariable implements Durable { + private readonly _id: DurableId; + + constructor(id: undefined extends T ? DurableIdInput : never); + constructor(options: { + id: DurableIdInput, + defaultValue: T, + }); + constructor(options: DurableIdInput | { + id: DurableIdInput, + defaultValue: T, + }) { + if (typeof options !== 'object' || !("id" in options)) { + options = { + id: options, + defaultValue: undefined as T, + }; + } + + assertDurableId(options.id); + this._id = options.id; + } + + async get() { + const { tableName, rowCondition } = await getMainTableRowInfo(this._id); + const result = await query` + SELECT value + FROM ${tableName} + WHERE ${rowCondition} + `; + return result.rows[0]?.value as T; + } + + async set(value: T) { + const { tableName, rowAllParentIds, rowId } = await getMainTableRowInfo(this._id); + await query` + INSERT INTO ${tableName} (all_parent_ids, id, value) + VALUES (${rowAllParentIds}, ${rowId}, ${JSON.stringify(value)}::jsonb) + ON CONFLICT (all_parent_ids, id) DO UPDATE SET value = ${JSON.stringify(value)}::jsonb + `; + } + + get [durableIdSymbol]() { + return this._id; + } +} diff --git a/packages/durables/src/errors.tsx b/packages/durables/src/errors.tsx new file mode 100644 index 0000000000..e84b7075eb --- /dev/null +++ b/packages/durables/src/errors.tsx @@ -0,0 +1,13 @@ +import { deindent, indentExceptFirstLine } from "@stackframe/stack-shared/dist/utils/strings"; + +export class DurablesError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = 'DurablesError'; + } +} + +export function createDurablesError(strings: TemplateStringsArray, ...args: any[]) { + const message = indentExceptFirstLine(deindent(strings, ...args), " ") + "\n"; + return (options?: ErrorOptions & { errorClass?: typeof DurablesError }) => new (options?.errorClass ?? DurablesError)(message, options); +} diff --git a/packages/durables/src/index.tsx b/packages/durables/src/index.tsx new file mode 100644 index 0000000000..107a4d9b70 --- /dev/null +++ b/packages/durables/src/index.tsx @@ -0,0 +1,5 @@ +export { Durable, DurableId, durableIdSymbol } from "./durables/durable"; +export { DurableTable } from "./durables/table"; +export { DurableVariable } from "./durables/variable"; +export { Sql, SqlIdentifier, SqlPart, SqlValue, Sqlizable, query, sql } from "./sql"; + diff --git a/packages/durables/src/serialize.tsx b/packages/durables/src/serialize.tsx new file mode 100644 index 0000000000..faccecb937 --- /dev/null +++ b/packages/durables/src/serialize.tsx @@ -0,0 +1 @@ +export type SerializableValue = string | number | boolean | null | undefined; diff --git a/packages/durables/src/sql.tsx b/packages/durables/src/sql.tsx new file mode 100644 index 0000000000..9ee04f37c7 --- /dev/null +++ b/packages/durables/src/sql.tsx @@ -0,0 +1,304 @@ +import { Extensions, PGlite, PGliteOptions } from "@electric-sql/pglite"; +import { repeat } from "@stackframe/stack-shared/dist/utils/arrays"; +import { typedAssign } from "@stackframe/stack-shared/dist/utils/objects"; +import { getDeindentInfo, indentExceptFirstLine, nicify } from "@stackframe/stack-shared/dist/utils/strings"; +import { ensureTrailingSlash } from "@stackframe/stack-shared/dist/utils/urls"; +import { fileURLToPath, pathToFileURL } from "url"; +import { DurablesError, createDurablesError } from "./errors"; +import { brandValue } from "./util/brands"; + +export const toSqlPartSymbol = Symbol.for('durables--toSqlPart'); + +let connectionString = typeof process !== 'undefined' ? + process.env.DURABLES_CONNECTION_STRING + || process.env.NEXT_PUBLIC_DURABLES_CONNECTION_STRING + || undefined + : undefined; + +export function setConnectionString(newConnectionString: string) { + connectionString = newConnectionString; +} + +export function getConnectionString() { + return connectionString; +} + +let dbPromise: ReturnType | undefined; +async function getDb() { + if (!dbPromise) { + dbPromise = createDb(); + } + return await dbPromise; +} + +const pgliteOptions: PGliteOptions = { + debug: 0, +}; + +async function createDb() { + if (!connectionString) { + throw createDurablesError` + You haven't provided a connection string to Durables. + + Please set the DURABLES_CONNECTION_STRING environment variable (eg. to 'memory://') or use setConnectionString() to set a global one in code. + `(); + } + + const baseUrl = typeof process !== 'undefined' ? pathToFileURL(process.cwd()) + : typeof window !== 'undefined' ? window.location.href + : undefined; + const connectionUrl = new URL(connectionString, baseUrl !== undefined ? ensureTrailingSlash(baseUrl) : undefined); + + switch (connectionUrl.protocol) { + case 'memory:': { + if (connectionString !== 'memory://') { + throw createDurablesError` + Invalid connection string: ${connectionString}. + + In-memory databases must use the connection string 'memory://'. + `(); + } + + return new PGlite("memory://", pgliteOptions); + } + case 'file:': { + const filePath = fileURLToPath(connectionUrl); + return new PGlite(filePath, pgliteOptions); + } + default: { + throw createDurablesError` + Unsupported connection string: ${connectionString}. + + The protocol ${connectionUrl.protocol} is not supported; try a valid connection string (eg. 'memory://'). + `(); + } + } +} + +export class DurablesSqlError extends DurablesError { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = 'DurablesSqlError'; + } +} + +function formatPostgresError(error: Error & { position?: number}, query: string, params: SqlValue[]) { + const position = error.position; + const beforePosition = query.slice(0, position); + const beforeLines = beforePosition.split('\n'); + const line = beforeLines[beforeLines.length - 1]; + const markerLine = repeat(line.length - 1, () => ' ').join('') + '^'; + const markerLineIndex = beforeLines.length; + + const queryLines = query.split('\n'); + + return createDurablesError` + ${error.message} + + Query: + ${queryLines.slice(0, markerLineIndex).join('\n')} + ${markerLine} + ${queryLines.slice(markerLineIndex).join('\n')} + + Params: + ${nicify(params)} + `({ cause: error, errorClass: DurablesSqlError }); +} + +export async function query(sql: Sql): Promise; +export async function query(sql: TemplateStringsArray, ...args: Sqlizable[]): Promise; +export async function query(q: Sql | TemplateStringsArray, ...args: Sqlizable[]): Promise { + if (Array.isArray(q)) { + return await query(sql(q as TemplateStringsArray, ...args)); + } else if (q instanceof _SqlImpl) { + const db = await getDb(); + const parts = await q.partsPromise; + + const query = [ + parts.rawStrings[0], + ...parts.params.flatMap((_, index) => [`$${index + 1}`, parts.rawStrings[index + 1]]), + ].join(' '); + const params = [...parts.params]; + + try { + const result = await db.query(query, params); + return result; + } catch (error) { + if (error instanceof Error && "code" in error && "file" in error && "position" in error) { + // likely to be a node-postgres error + throw formatPostgresError(error as any, query, params); + } + } + } else { + throw createDurablesError` + Invalid SQL query: ${q} + `(); + } +} + +type Parts = { + readonly rawStrings: readonly string[], + readonly params: readonly SqlValue[], +}; + +class _SqlPartImpl { + public readonly __sqlPartBrand = brandValue; + public readonly partsPromise: Promise; + + constructor(partsPromise: _SqlPartImpl['partsPromise']) { + this.partsPromise = (async () => { + const parts = await partsPromise; + if (parts.rawStrings.length === 0) { + throw createDurablesError` + Raw strings must have at least one element + `(); + } + if (parts.rawStrings.length !== parts.params.length + 1) { + throw createDurablesError` + Raw strings must have one more element than params + `(); + } + return parts; + })(); + } + + map(fn: (value: Parts) => Promise): SqlPart { + return new _SqlPartImpl(this.partsPromise.then(fn)); + } +} + +class _SqlIdentifierImpl extends _SqlPartImpl { + public __sqlIdentifierBrand = brandValue; + + constructor(...args: ConstructorParameters) { + super(...args); + } + + map(fn: (value: Parts) => Promise): SqlIdentifier { + return new _SqlIdentifierImpl(this.partsPromise.then(fn)); + } +} + +class _SqlImpl extends _SqlPartImpl { + public __sqlBrand = brandValue; + + constructor(...args: ConstructorParameters) { + super(...args); + } + + map(fn: (value: Parts) => Promise): Sql { + return new _SqlImpl(this.partsPromise.then(fn)); + } +} + +export type SqlPart = _SqlPartImpl; +export type SqlIdentifier = _SqlIdentifierImpl; +export type Sql = _SqlImpl; +export type SqlValue = string | number | boolean | null | SqlValue[]; +export type Sqlizable = SqlPart | SqlValue | { [toSqlPartSymbol]: () => Promise }; + +function sqlize(value: Sqlizable): SqlPart { + if (value instanceof _SqlPartImpl) { + return value; + } + if (typeof value === 'object' && value !== null && toSqlPartSymbol in value) { + return sql.deferred(value[toSqlPartSymbol]().then(sqlize)); + } + return sql.value(value); +} + +function indentSql(value: SqlPart, indentation: string): SqlPart { + return new _SqlPartImpl((async () => { + const { rawStrings, params } = await value.partsPromise; + return { + rawStrings: rawStrings.map(raw => indentExceptFirstLine(raw, indentation)), + params, + }; + })()); +} + +export const sql = typedAssign( + (strings: TemplateStringsArray, ...args: Sqlizable[]): Sql => { + const [deindentedStrings, deindentInfo] = getDeindentInfo(strings, ...args); + let result = sql.raw(deindentedStrings[0]); + for (let i = 1; i < deindentedStrings.length; i++) { + const info = deindentInfo[i-1]; + result = sql.concat( + result, + indentSql(sqlize(info.value), info.indentation), + sql.raw(deindentedStrings[i]), + ); + } + return result; + }, + { + empty: () => sql``, + raw: (raw: string): Sql => new _SqlImpl(Promise.resolve({ + rawStrings: [raw], + params: [], + })), + identifier: (identifier: string): SqlIdentifier => { + if (identifier.includes(String.fromCharCode(0))) { + throw createDurablesError` + Identifier cannot contain NUL characters + `(); + } + return new _SqlIdentifierImpl(Promise.resolve({ + rawStrings: [`"${identifier.replace(/"/g, '""')}"`], + params: [], + })); + }, + isValue: (value: unknown): value is SqlValue => typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || value === null || Array.isArray(value), + value: (value: SqlValue): SqlPart => { + if (!sql.isValue(value)) { + throw createDurablesError` + Invalid SQL value: ${nicify(value)} + `(); + } + return new _SqlPartImpl(Promise.resolve({ + rawStrings: ['', ''], + params: [value], + })); + }, + deferred: sqlDeferred, + concat: (...parts: SqlPart[]): Sql => { + if (parts.length === 2) { + return new _SqlImpl((async () => { + const first = await parts[0].partsPromise; + const second = await parts[1].partsPromise; + return { + rawStrings: [ + ...first.rawStrings.slice(0, -1), + first.rawStrings[first.rawStrings.length - 1] + second.rawStrings[0], + ...second.rawStrings.slice(1) + ], + params: [ + ...first.params, + ...second.params, + ], + }; + })()); + } else { + let result = sql``; + for (const part of parts) { + result = sql.concat(result, part); + } + return result; + } + }, + join: (parts: SqlPart[], separator: SqlPart) => { + if (parts.length === 0) { + return sql``; + } + return sql.concat(parts[0], ...parts.flatMap(part => [separator, part])); + } + } +); + +// we need to define this here because we can't specify function overloads in the object literal without casting +// (or at least I don't know how) +function sqlDeferred(promise: Promise): Sql; +function sqlDeferred(promise: Promise): SqlPart; +function sqlDeferred(promise: Promise): SqlPart { + return new _SqlImpl(promise.then(part => part.partsPromise)); +} diff --git a/packages/durables/src/util/brands.tsx b/packages/durables/src/util/brands.tsx new file mode 100644 index 0000000000..313e25872c --- /dev/null +++ b/packages/durables/src/util/brands.tsx @@ -0,0 +1 @@ +export const brandValue = 'durables-brand-value'; diff --git a/packages/durables/src/util/hashes.tsx b/packages/durables/src/util/hashes.tsx new file mode 100644 index 0000000000..a81ab53f87 --- /dev/null +++ b/packages/durables/src/util/hashes.tsx @@ -0,0 +1,12 @@ +import { encodeBase32 } from "@stackframe/stack-shared/dist/utils/bytes"; +import { sha512 } from "@stackframe/stack-shared/dist/utils/hashes"; + + +/** + * Returns a 20 character base32 encoded string that is a hash of the input. + */ +export async function generateHashedCode(uniqueKind: string, str: string): Promise { + const toHash = JSON.stringify(["--stack-durables-generateHashedCode", uniqueKind, str]); + const hash = encodeBase32(await sha512(toHash)); + return hash.slice(0, 20); +} diff --git a/packages/durables/tsconfig.json b/packages/durables/tsconfig.json new file mode 100644 index 0000000000..66a20ac9f8 --- /dev/null +++ b/packages/durables/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "outDir": "dist", + "rootDir": "", + "declaration": true, + "target": "ES2021", + "lib": ["DOM", "DOM.Iterable", "ES2021", "ES2022.Error"], + "module": "ES2020", + "moduleResolution": "Bundler", + "esModuleInterop": true, + "noErrorTruncation": true, + "skipLibCheck": true, + "strict": true, + "sourceMap": true, + "declarationMap": true + }, + "include": ["next-env.d.ts", "src/**/*", ".next/types/**/*.ts", "examples/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/durables/tsup.config.ts b/packages/durables/tsup.config.ts new file mode 100644 index 0000000000..929cbdd8bf --- /dev/null +++ b/packages/durables/tsup.config.ts @@ -0,0 +1,37 @@ +import { defineConfig, Options } from 'tsup'; +import packageJson from './package.json'; + +const customNoExternal = new Set([ + "oauth4webapi", +]); + +const config: Options = { + entryPoints: ['src/**/*.(ts|tsx|js|jsx)'], + sourcemap: true, + clean: false, + noExternal: [...customNoExternal], + dts: true, + outDir: 'dist', + format: ['esm', 'cjs'], + legacyOutput: true, + env: { + STACK_COMPILE_TIME_CLIENT_PACKAGE_VERSION: `js ${packageJson.name}@${packageJson.version}`, + }, + esbuildPlugins: [ + { + name: 'durables tsup plugin (private)', + setup(build) { + build.onResolve({ filter: /^.*$/m }, async (args) => { + if (args.kind === "entry-point" || customNoExternal.has(args.path)) { + return undefined; + } + return { + external: true, + }; + }); + }, + }, + ], +}; + +export default defineConfig(config); diff --git a/packages/stack-shared/src/utils/arrays.tsx b/packages/stack-shared/src/utils/arrays.tsx index 8ecba6e066..318e057528 100644 --- a/packages/stack-shared/src/utils/arrays.tsx +++ b/packages/stack-shared/src/utils/arrays.tsx @@ -16,6 +16,10 @@ export function isShallowEqual(a: readonly any[], b: readonly any[]): boolean { return true; } +export function repeat(count: number, func: (index: number) => T): T[] { + return Array(count).fill(null).map((_, index) => func(index)); +} + /** * Ponyfill for ES2023's findLastIndex. */ diff --git a/packages/stack-shared/src/utils/hashes.tsx b/packages/stack-shared/src/utils/hashes.tsx index 4dd8fdbe04..dd874ae789 100644 --- a/packages/stack-shared/src/utils/hashes.tsx +++ b/packages/stack-shared/src/utils/hashes.tsx @@ -3,9 +3,15 @@ import { StackAssertionError } from './errors'; export async function sha512(input: Uint8Array | string) { const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input; - return await crypto.subtle.digest("SHA-512", bytes).then(buf => { - return Array.prototype.map.call(new Uint8Array(buf), x=>(('00'+x.toString(16)).slice(-2))).join(''); - }); + return new Uint8Array(await crypto.subtle.digest("SHA-512", bytes)); +} + +/** + * Hashes the input using SHA-512 and returns it as a hex string. + */ +export async function sha512ToHex(input: Uint8Array | string) { + const buf = await sha512(input); + return Array.prototype.map.call(new Uint8Array(buf), x=>(('00'+x.toString(16)).slice(-2))).join(''); } export async function hashPassword(password: string) { diff --git a/packages/stack-shared/src/utils/strings.tsx b/packages/stack-shared/src/utils/strings.tsx index 8be53e50d5..659700acba 100644 --- a/packages/stack-shared/src/utils/strings.tsx +++ b/packages/stack-shared/src/utils/strings.tsx @@ -75,12 +75,29 @@ export function templateIdentity(strings: TemplateStringsArray | readonly string return strings.slice(1).reduce((result, string, i) => `${result}${values[i] ?? "n/a"}${string}`, strings[0]); } +export function indent(str: string, indentation: string): `${typeof indentation}${typeof str}` { + return indentation + indentExceptFirstLine(str, indentation); +} + +export function indentExceptFirstLine(str: string, indentation: string): `${typeof indentation}${typeof str}` { + if (typeof str !== "string") throw new Error(`Invalid string ${nicify(str)}`); + return str.replaceAll("\n", `\n${indentation}`); +} export function deindent(code: string): string; export function deindent(strings: TemplateStringsArray | readonly string[], ...values: any[]): string; export function deindent(strings: string | readonly string[], ...values: any[]): string { if (typeof strings === "string") return deindent([strings]); - if (strings.length === 0) return ""; + + const [deindentedStrings, deindentInfo] = getDeindentInfo(strings, ...values); + return templateIdentity( + deindentedStrings, + ...deindentInfo.map(({ value, indentation }) => indentExceptFirstLine(`${value}`, indentation)), + ); +} + +export function getDeindentInfo(strings: readonly string[], ...values: any[]): [string[], { value: any, indentation: string }[]] { + if (strings.length === 0 && values.length === 0) return [[], []]; if (values.length !== strings.length - 1) throw new Error("Invalid number of values; must be one less than strings"); const trimmedStrings = [...strings]; @@ -104,10 +121,10 @@ export function deindent(strings: string | readonly string[], ...values: any[]): const indentedValues = values.map((value, i) => { const firstLineIndentation = getWhitespacePrefix(deindentedStrings[i].split("\n").at(-1)!); - return `${value}`.replaceAll("\n", `\n${firstLineIndentation}`); + return { value, indentation: firstLineIndentation }; }); - return templateIdentity(deindentedStrings, ...indentedValues); + return [deindentedStrings, indentedValues]; } export function extractScopes(scope: string, removeDuplicates=true): string[] { diff --git a/packages/stack-shared/src/utils/urls.tsx b/packages/stack-shared/src/utils/urls.tsx index 272fb86c85..2136c54513 100644 --- a/packages/stack-shared/src/utils/urls.tsx +++ b/packages/stack-shared/src/utils/urls.tsx @@ -29,6 +29,17 @@ export function isRelative(url: string) { return true; } +/** + * Relative URL lookups work differently depending on whether the URL has a trailing slash or not. + * + * This function ensures that the URL has a trailing slash, so that relative lookups work consistently. + */ +export function ensureTrailingSlash(url: URL | string) { + if (typeof url === "string") url = new URL(url); + if (url.pathname.endsWith("/")) return url; + return new URL(url.toString() + "/"); +} + export function getRelativePart(url: URL) { return url.pathname + url.search + url.hash; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6aae3d3238..7bc7caba51 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -729,6 +729,25 @@ importers: specifier: 5.3.3 version: 5.3.3 + packages/durables: + dependencies: + '@electric-sql/pglite': + specifier: ^0.2.15 + version: 0.2.15 + '@stackframe/stack-shared': + specifier: workspace:* + version: link:../stack-shared + devDependencies: + esbuild: + specifier: ^0.20.2 + version: 0.20.2 + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + tsup: + specifier: ^8.0.2 + version: 8.3.5(@swc/core@1.3.101)(jiti@1.21.6)(postcss@8.4.47)(tsx@4.16.2)(typescript@5.3.3)(yaml@2.6.0) + packages/init-stack: dependencies: inquirer: @@ -1489,6 +1508,9 @@ packages: resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} + '@electric-sql/pglite@0.2.15': + resolution: {integrity: sha512-Jiq31Dnk+rg8rMhcSxs4lQvHTyizNo5b269c1gCC3ldQ0sCLrNVPGzy+KnmonKy1ZArTUuXZf23/UamzFMKVaA==} + '@emnapi/core@0.45.0': resolution: {integrity: sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw==} @@ -11735,6 +11757,8 @@ snapshots: '@discoveryjs/json-ext@0.5.7': {} + '@electric-sql/pglite@0.2.15': {} + '@emnapi/core@0.45.0': dependencies: tslib: 2.8.1