@objectstack/client 4.0.4 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
2
  import { isFilterAST } from "@objectstack/spec/data";
3
- import { createLogger } from "@objectstack/core";
3
+ import { createLogger } from "@objectstack/core/logger";
4
4
 
5
5
  // src/realtime-api.ts
6
6
  var RealtimeAPI = class {
@@ -631,6 +631,745 @@ var ObjectStackClient = class {
631
631
  return this.unwrapResponse(res);
632
632
  }
633
633
  };
634
+ /**
635
+ * Environment Management Services
636
+ *
637
+ * Environments are the v4.1+ isolation primitive — each project owns a
638
+ * physically separate data-plane database. All Studio-level switching goes
639
+ * through this API.
640
+ *
641
+ * Endpoints:
642
+ * - GET /api/v1/cloud/projects → list environments
643
+ * - GET /api/v1/cloud/projects/:id → get one (with database info)
644
+ * - POST /api/v1/cloud/projects → provision a new project
645
+ * - PATCH /api/v1/cloud/projects/:id → update (displayName, plan, status, …)
646
+ * - POST /api/v1/cloud/projects/:id/activate → set as session's active project
647
+ * - POST /api/v1/cloud/projects/:id/credentials/rotate → rotate credential
648
+ *
649
+ * @see docs/adr/0002-project-database-isolation.md
650
+ */
651
+ this.projects = {
652
+ /**
653
+ * List environments visible to the current session. Optionally filter
654
+ * by organization (control-plane query — not routed through a data-plane DB).
655
+ */
656
+ list: async (filters) => {
657
+ const params = new URLSearchParams();
658
+ if (filters?.organization_id) params.set("organizationId", filters.organization_id);
659
+ if (filters?.env_type) params.set("envType", filters.env_type);
660
+ if (filters?.status) params.set("status", filters.status);
661
+ const qs = params.toString();
662
+ const url = `${this.baseUrl}/api/v1/cloud/projects${qs ? "?" + qs : ""}`;
663
+ const res = await this.fetch(url);
664
+ return this.unwrapResponse(res);
665
+ },
666
+ /**
667
+ * Get a single project (joined with its database and membership row).
668
+ */
669
+ get: async (id) => {
670
+ const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(id)}`);
671
+ return this.unwrapResponse(res);
672
+ },
673
+ /**
674
+ * Provision a new project. Delegates to
675
+ * `ProjectProvisioningService.provisionProject` on the server.
676
+ */
677
+ create: async (req) => {
678
+ const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects`, {
679
+ method: "POST",
680
+ body: JSON.stringify(req)
681
+ });
682
+ return this.unwrapResponse(res);
683
+ },
684
+ /**
685
+ * Update a project (display_name, plan, status, is_default, metadata).
686
+ */
687
+ update: async (id, patch) => {
688
+ const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(id)}`, {
689
+ method: "PATCH",
690
+ body: JSON.stringify(patch)
691
+ });
692
+ return this.unwrapResponse(res);
693
+ },
694
+ /**
695
+ * Cascade-delete a project: cleans up credential/member/package_installation
696
+ * rows, releases the physical database via the provisioning adapter, and
697
+ * removes the `sys_project` row. Default projects require `force: true`.
698
+ */
699
+ delete: async (id, opts) => {
700
+ const qs = opts?.force ? "?force=1" : "";
701
+ const res = await this.fetch(
702
+ `${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(id)}${qs}`,
703
+ { method: "DELETE" }
704
+ );
705
+ return this.unwrapResponse(res);
706
+ },
707
+ /**
708
+ * Activate this project for the current session. The server writes
709
+ * `active_environment_id` on the better-auth session; subsequent requests
710
+ * are routed to this project's database.
711
+ */
712
+ activate: async (id) => {
713
+ const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(id)}/activate`, {
714
+ method: "POST"
715
+ });
716
+ return this.unwrapResponse(res);
717
+ },
718
+ /**
719
+ * Rotate the active database credential for this project.
720
+ */
721
+ rotateCredential: async (id, plaintext) => {
722
+ const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(id)}/credentials/rotate`, {
723
+ method: "POST",
724
+ body: JSON.stringify({ plaintext })
725
+ });
726
+ return this.unwrapResponse(res);
727
+ },
728
+ /**
729
+ * Update the hostname bound to this project. Validates format and
730
+ * uniqueness server-side; invalidates the dispatcher's routing cache.
731
+ */
732
+ updateHostname: async (id, hostname) => {
733
+ const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(id)}/hostname`, {
734
+ method: "POST",
735
+ body: JSON.stringify({ hostname })
736
+ });
737
+ return this.unwrapResponse(res);
738
+ },
739
+ /**
740
+ * Update the visibility of this project ('private' | 'public').
741
+ * `private` (default) hides the project from /pub/v1 enumeration but
742
+ * still allows anonymous artifact downloads when the URL includes an
743
+ * exact `?commit=<id>` (share-by-link). `public` lists the project and
744
+ * freely exposes all revisions.
745
+ */
746
+ updateVisibility: async (id, visibility) => {
747
+ const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(id)}`, {
748
+ method: "PATCH",
749
+ body: JSON.stringify({ visibility })
750
+ });
751
+ return this.unwrapResponse(res);
752
+ },
753
+ /**
754
+ * List published artifact revisions for a project. Each revision has
755
+ * an immutable commitId (content-addressable) and storage_key.
756
+ * Optional `branch` filter narrows to a single logical branch
757
+ * (default branch `main` also matches rows with NULL `branch`).
758
+ */
759
+ listRevisions: async (id, opts) => {
760
+ const params = new URLSearchParams();
761
+ if (opts?.limit) params.set("limit", String(opts.limit));
762
+ if (opts?.cursor) params.set("cursor", opts.cursor);
763
+ if (opts?.branch) params.set("branch", opts.branch);
764
+ const qs = params.toString();
765
+ const res = await this.fetch(
766
+ `${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(id)}/revisions${qs ? `?${qs}` : ""}`
767
+ );
768
+ return this.unwrapResponse(res);
769
+ },
770
+ /**
771
+ * List logical branches for a project. Each branch has a head commit
772
+ * (latest published revision on that branch) and a count of revisions.
773
+ * Branches without a head row (e.g. all rows demoted) are omitted.
774
+ */
775
+ listBranches: async (id) => {
776
+ const res = await this.fetch(
777
+ `${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(id)}/branches`
778
+ );
779
+ return this.unwrapResponse(res);
780
+ },
781
+ /**
782
+ * Rename a branch. Updates every revision row in `from` to `to`.
783
+ * 409 if `to` already has rows.
784
+ */
785
+ renameBranch: async (id, from, to) => {
786
+ const res = await this.fetch(
787
+ `${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(id)}/branches/${encodeURIComponent(from)}/rename`,
788
+ {
789
+ method: "POST",
790
+ headers: { "content-type": "application/json" },
791
+ body: JSON.stringify({ newName: to })
792
+ }
793
+ );
794
+ return this.unwrapResponse(res);
795
+ },
796
+ /**
797
+ * Delete (demote) a branch. Soft-removal — clears `is_branch_head` on
798
+ * every row in this branch; the revisions themselves remain. The
799
+ * `main` branch and any branch carrying the active revision cannot be
800
+ * deleted.
801
+ */
802
+ deleteBranch: async (id, name) => {
803
+ const res = await this.fetch(
804
+ `${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(id)}/branches/${encodeURIComponent(name)}`,
805
+ { method: "DELETE" }
806
+ );
807
+ return this.unwrapResponse(res);
808
+ },
809
+ /**
810
+ * Activate (rollback to) a previously-published revision by commit id.
811
+ * Marks the target revision is_current=true and demotes the prior one.
812
+ */
813
+ activateRevision: async (id, commitId) => {
814
+ const res = await this.fetch(
815
+ `${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(id)}/revisions/${encodeURIComponent(commitId)}/activate`,
816
+ { method: "POST" }
817
+ );
818
+ return this.unwrapResponse(res);
819
+ },
820
+ /**
821
+ * Retry provisioning for a project stuck in `failed` (or
822
+ * `provisioning`) state. The server re-runs the driver handshake; on
823
+ * success the project flips to `active`, on failure it stays
824
+ * `failed` with `metadata.provisioningError` updated.
825
+ */
826
+ retryProvisioning: async (id) => {
827
+ const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(id)}/retry`, {
828
+ method: "POST"
829
+ });
830
+ return this.unwrapResponse(res);
831
+ },
832
+ /**
833
+ * List members of a project (per-project RBAC).
834
+ */
835
+ listMembers: async (id) => {
836
+ const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(id)}/members`);
837
+ return this.unwrapResponse(res);
838
+ },
839
+ /**
840
+ * Invite a member to a project. Caller must be `owner` or `admin`.
841
+ * Pass either `email` (resolved against the user table) or `user_id`.
842
+ * Returns `{ member, alreadyMember }` — `alreadyMember=true` means the
843
+ * row already existed; the call is idempotent.
844
+ */
845
+ addMember: async (id, payload) => {
846
+ const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(id)}/members`, {
847
+ method: "POST",
848
+ headers: { "content-type": "application/json" },
849
+ body: JSON.stringify(payload)
850
+ });
851
+ return this.unwrapResponse(res);
852
+ },
853
+ /**
854
+ * Update a member's role. Caller must be `owner` or `admin`. Demoting
855
+ * the last `owner` returns 409.
856
+ */
857
+ updateMemberRole: async (id, memberId, role) => {
858
+ const res = await this.fetch(
859
+ `${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(id)}/members/${encodeURIComponent(memberId)}`,
860
+ {
861
+ method: "PATCH",
862
+ headers: { "content-type": "application/json" },
863
+ body: JSON.stringify({ role })
864
+ }
865
+ );
866
+ return this.unwrapResponse(res);
867
+ },
868
+ /**
869
+ * Remove a member. Owners/admins may remove anyone; non-privileged
870
+ * users may only remove themselves. Removing the last `owner` returns 409.
871
+ */
872
+ removeMember: async (id, memberId) => {
873
+ const res = await this.fetch(
874
+ `${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(id)}/members/${encodeURIComponent(memberId)}`,
875
+ { method: "DELETE" }
876
+ );
877
+ return this.unwrapResponse(res);
878
+ },
879
+ /**
880
+ * List ObjectQL drivers registered on the server. Useful for populating a
881
+ * driver selector when provisioning a new project (memory / turso /
882
+ * future sql drivers). Returned `name` is the short alias (e.g. `memory`,
883
+ * `turso`); `driverId` is the full FQN (e.g. `com.objectstack.driver.memory`).
884
+ */
885
+ listDrivers: async () => {
886
+ const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/drivers`);
887
+ return this.unwrapResponse(res);
888
+ },
889
+ /**
890
+ * List available project templates. Templates are seeded into the project
891
+ * database once at provisioning time when `template_id` is supplied.
892
+ */
893
+ listTemplates: async () => {
894
+ const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/templates`);
895
+ return this.unwrapResponse(res);
896
+ },
897
+ /**
898
+ * Per-project package installation management (Power Apps "solution" model).
899
+ * Install records are stored in the environment's own database.
900
+ */
901
+ packages: {
902
+ /** List all packages installed in a specific project. */
903
+ list: async (envId) => {
904
+ const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(envId)}/packages`);
905
+ return this.unwrapResponse(res);
906
+ },
907
+ /** Install a package into the project. */
908
+ install: async (envId, body) => {
909
+ const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(envId)}/packages`, {
910
+ method: "POST",
911
+ body: JSON.stringify(body)
912
+ });
913
+ return this.unwrapResponse(res);
914
+ },
915
+ /** Get a single installation record. */
916
+ get: async (envId, pkgId) => {
917
+ const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(envId)}/packages/${encodeURIComponent(pkgId)}`);
918
+ return this.unwrapResponse(res);
919
+ },
920
+ /** Enable a previously disabled package. */
921
+ enable: async (envId, pkgId) => {
922
+ const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(envId)}/packages/${encodeURIComponent(pkgId)}/enable`, {
923
+ method: "PATCH"
924
+ });
925
+ return this.unwrapResponse(res);
926
+ },
927
+ /** Disable an installed package (metadata will not be loaded). */
928
+ disable: async (envId, pkgId) => {
929
+ const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(envId)}/packages/${encodeURIComponent(pkgId)}/disable`, {
930
+ method: "PATCH"
931
+ });
932
+ return this.unwrapResponse(res);
933
+ },
934
+ /** Uninstall a package from the project. Forbidden for scope=platform packages. */
935
+ uninstall: async (envId, pkgId) => {
936
+ const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(envId)}/packages/${encodeURIComponent(pkgId)}`, {
937
+ method: "DELETE"
938
+ });
939
+ return this.unwrapResponse(res);
940
+ },
941
+ /** Upgrade an installed package to a newer version. */
942
+ upgrade: async (envId, pkgId, targetVersion) => {
943
+ const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(envId)}/packages/${encodeURIComponent(pkgId)}/upgrade`, {
944
+ method: "POST",
945
+ body: JSON.stringify({ targetVersion })
946
+ });
947
+ return this.unwrapResponse(res);
948
+ }
949
+ }
950
+ };
951
+ /**
952
+ * Organization Services
953
+ *
954
+ * Thin wrapper around better-auth's organization plugin endpoints, which
955
+ * are mounted under `/api/v1/auth/organization/**`. Used by the Studio
956
+ * OrganizationSwitcher and the /orgs management routes.
957
+ */
958
+ this.organizations = {
959
+ /**
960
+ * List organizations the current user belongs to.
961
+ * GET /api/v1/auth/organization/list
962
+ */
963
+ list: async () => {
964
+ const route = this.getRoute("auth");
965
+ const res = await this.fetch(`${this.baseUrl}${route}/organization/list`);
966
+ const data = await res.json();
967
+ const orgs = Array.isArray(data) ? data : data?.data ?? [];
968
+ return { organizations: orgs };
969
+ },
970
+ /**
971
+ * Create a new organization.
972
+ * POST /api/v1/auth/organization/create
973
+ */
974
+ create: async (req) => {
975
+ const route = this.getRoute("auth");
976
+ const res = await this.fetch(`${this.baseUrl}${route}/organization/create`, {
977
+ method: "POST",
978
+ body: JSON.stringify(req)
979
+ });
980
+ return res.json();
981
+ },
982
+ /**
983
+ * Update an existing organization.
984
+ * POST /api/v1/auth/organization/update
985
+ *
986
+ * better-auth requires the caller to be an owner/admin (server-side
987
+ * enforcement); the body shape is `{ organizationId, data: {...} }`.
988
+ */
989
+ update: async (organizationId, data) => {
990
+ const route = this.getRoute("auth");
991
+ const res = await this.fetch(`${this.baseUrl}${route}/organization/update`, {
992
+ method: "POST",
993
+ body: JSON.stringify({ organizationId, data })
994
+ });
995
+ return res.json();
996
+ },
997
+ /**
998
+ * Set the active organization on the current session. The server writes
999
+ * `activeOrganizationId` on the better-auth session, which downstream
1000
+ * handlers (e.g. `EnvironmentProvisioningService`) consult.
1001
+ *
1002
+ * POST /api/v1/auth/organization/set-active
1003
+ */
1004
+ setActive: async (organizationId) => {
1005
+ const route = this.getRoute("auth");
1006
+ const res = await this.fetch(`${this.baseUrl}${route}/organization/set-active`, {
1007
+ method: "POST",
1008
+ body: JSON.stringify({ organizationId })
1009
+ });
1010
+ return res.json();
1011
+ },
1012
+ /**
1013
+ * Get full organization detail (members, invitations, teams).
1014
+ * GET /api/v1/auth/organization/get-full-organization?organizationId=...
1015
+ */
1016
+ get: async (organizationId) => {
1017
+ const route = this.getRoute("auth");
1018
+ const res = await this.fetch(
1019
+ `${this.baseUrl}${route}/organization/get-full-organization?organizationId=${encodeURIComponent(organizationId)}`
1020
+ );
1021
+ return res.json();
1022
+ },
1023
+ /**
1024
+ * List members of an organization.
1025
+ */
1026
+ listMembers: async (organizationId) => {
1027
+ const route = this.getRoute("auth");
1028
+ const res = await this.fetch(
1029
+ `${this.baseUrl}${route}/organization/list-members?organizationId=${encodeURIComponent(organizationId)}`
1030
+ );
1031
+ return res.json();
1032
+ },
1033
+ /**
1034
+ * Invite a user to the organization.
1035
+ */
1036
+ invite: async (req) => {
1037
+ const route = this.getRoute("auth");
1038
+ const res = await this.fetch(`${this.baseUrl}${route}/organization/invite-member`, {
1039
+ method: "POST",
1040
+ body: JSON.stringify(req)
1041
+ });
1042
+ return res.json();
1043
+ },
1044
+ /**
1045
+ * Leave the given organization.
1046
+ */
1047
+ leave: async (organizationId) => {
1048
+ const route = this.getRoute("auth");
1049
+ const res = await this.fetch(`${this.baseUrl}${route}/organization/leave`, {
1050
+ method: "POST",
1051
+ body: JSON.stringify({ organizationId })
1052
+ });
1053
+ return res.json();
1054
+ },
1055
+ /**
1056
+ * Delete an organization via better-auth's organization plugin.
1057
+ *
1058
+ * POST /api/v1/auth/organization/delete
1059
+ *
1060
+ * better-auth removes the organization row, all members, and all
1061
+ * pending invitations. Project teardown (per-project DBs, etc.) is
1062
+ * handled server-side by hooks attached to the organization plugin.
1063
+ */
1064
+ delete: async (organizationId) => {
1065
+ const route = this.getRoute("auth");
1066
+ const res = await this.fetch(`${this.baseUrl}${route}/organization/delete`, {
1067
+ method: "POST",
1068
+ body: JSON.stringify({ organizationId })
1069
+ });
1070
+ return res.json();
1071
+ },
1072
+ /**
1073
+ * Remove a member from an organization.
1074
+ *
1075
+ * better-auth: POST /organization/remove-member
1076
+ * Body: `{ memberIdOrEmail, organizationId? }` — note the parameter is the
1077
+ * **member id** (the row id from `member` table) or the user's email; it
1078
+ * is *not* the bare `userId`. Server enforces owner/admin permission.
1079
+ */
1080
+ removeMember: async (organizationId, params) => {
1081
+ const route = this.getRoute("auth");
1082
+ const res = await this.fetch(`${this.baseUrl}${route}/organization/remove-member`, {
1083
+ method: "POST",
1084
+ body: JSON.stringify({ memberIdOrEmail: params.memberIdOrEmail, organizationId })
1085
+ });
1086
+ return res.json();
1087
+ },
1088
+ /**
1089
+ * Change a member's role in an organization (owner/admin only).
1090
+ *
1091
+ * better-auth: POST /organization/update-member-role
1092
+ * Body: `{ memberId, role, organizationId? }`. The `memberId` is the
1093
+ * `member` table row id (not user id). `role` is one of the configured
1094
+ * organisation roles (default: `owner | admin | member`).
1095
+ */
1096
+ updateMemberRole: async (organizationId, params) => {
1097
+ const route = this.getRoute("auth");
1098
+ const res = await this.fetch(`${this.baseUrl}${route}/organization/update-member-role`, {
1099
+ method: "POST",
1100
+ body: JSON.stringify({ memberId: params.memberId, role: params.role, organizationId })
1101
+ });
1102
+ return res.json();
1103
+ },
1104
+ /**
1105
+ * Look up the calling user's membership row in the given organisation.
1106
+ * Useful for permission checks on the client without having to scan the
1107
+ * full member list.
1108
+ *
1109
+ * better-auth: GET /organization/get-active-member?organizationId=…
1110
+ */
1111
+ getActiveMember: async (organizationId) => {
1112
+ const route = this.getRoute("auth");
1113
+ const res = await this.fetch(
1114
+ `${this.baseUrl}${route}/organization/get-active-member?organizationId=${encodeURIComponent(organizationId)}`
1115
+ );
1116
+ return res.json();
1117
+ },
1118
+ /**
1119
+ * Invitation lifecycle — wraps better-auth's organization-plugin
1120
+ * invitation endpoints. Always go through here instead of writing to
1121
+ * `sys_invitation` via the data API: the better-auth writers handle
1122
+ * status transitions, expiry, dedupe, and the `sendInvitationEmail`
1123
+ * side-effect that the auth-manager wires up.
1124
+ */
1125
+ invitations: {
1126
+ /**
1127
+ * List pending/accepted/canceled invitations for an organization.
1128
+ * Requires owner/admin role on that org.
1129
+ *
1130
+ * better-auth: GET /organization/list-invitations?organizationId=…
1131
+ */
1132
+ list: async (organizationId) => {
1133
+ const route = this.getRoute("auth");
1134
+ const res = await this.fetch(
1135
+ `${this.baseUrl}${route}/organization/list-invitations?organizationId=${encodeURIComponent(organizationId)}`
1136
+ );
1137
+ const data = await res.json();
1138
+ const invitations = Array.isArray(data) ? data : data?.data ?? data?.invitations ?? [];
1139
+ return { invitations };
1140
+ },
1141
+ /**
1142
+ * List the **current user's** incoming invitations across every
1143
+ * organisation. Used by the per-user "Invitations" inbox page.
1144
+ *
1145
+ * better-auth: GET /organization/list-user-invitations
1146
+ */
1147
+ listMine: async () => {
1148
+ const route = this.getRoute("auth");
1149
+ const res = await this.fetch(`${this.baseUrl}${route}/organization/list-user-invitations`);
1150
+ const data = await res.json();
1151
+ const invitations = Array.isArray(data) ? data : data?.data ?? data?.invitations ?? [];
1152
+ return { invitations };
1153
+ },
1154
+ /** better-auth: POST /organization/cancel-invitation */
1155
+ cancel: async (invitationId) => {
1156
+ const route = this.getRoute("auth");
1157
+ const res = await this.fetch(`${this.baseUrl}${route}/organization/cancel-invitation`, {
1158
+ method: "POST",
1159
+ body: JSON.stringify({ invitationId })
1160
+ });
1161
+ return res.json();
1162
+ },
1163
+ /** better-auth: POST /organization/accept-invitation */
1164
+ accept: async (invitationId) => {
1165
+ const route = this.getRoute("auth");
1166
+ const res = await this.fetch(`${this.baseUrl}${route}/organization/accept-invitation`, {
1167
+ method: "POST",
1168
+ body: JSON.stringify({ invitationId })
1169
+ });
1170
+ return res.json();
1171
+ },
1172
+ /** better-auth: POST /organization/reject-invitation */
1173
+ reject: async (invitationId) => {
1174
+ const route = this.getRoute("auth");
1175
+ const res = await this.fetch(`${this.baseUrl}${route}/organization/reject-invitation`, {
1176
+ method: "POST",
1177
+ body: JSON.stringify({ invitationId })
1178
+ });
1179
+ return res.json();
1180
+ },
1181
+ /**
1182
+ * "Resend" an invitation. better-auth has no first-class resend
1183
+ * endpoint, so we implement it as cancel-then-invite: cancel the old
1184
+ * row (so its status flips to `canceled` and audit hooks fire), then
1185
+ * issue a fresh invite. The new invite re-runs `sendInvitationEmail`
1186
+ * on the server, so the recipient gets a brand-new accept URL.
1187
+ *
1188
+ * If `cancel()` fails (e.g. invite already accepted) the error is
1189
+ * re-thrown without re-inviting.
1190
+ */
1191
+ resend: async (invitation) => {
1192
+ if (invitation.id) {
1193
+ try {
1194
+ await this.organizations.invitations.cancel(invitation.id);
1195
+ } catch {
1196
+ }
1197
+ }
1198
+ return this.organizations.invite({
1199
+ email: invitation.email,
1200
+ role: invitation.role ?? "member",
1201
+ organizationId: invitation.organizationId
1202
+ });
1203
+ }
1204
+ },
1205
+ /**
1206
+ * Team management — only available when the organisation plugin is
1207
+ * configured with `teams: { enabled: true }` on the server. Calls return
1208
+ * a 4xx if teams aren't enabled; UI should hide the section in that case.
1209
+ */
1210
+ teams: {
1211
+ /** better-auth: GET /organization/list-teams?organizationId=… */
1212
+ list: async (organizationId) => {
1213
+ const route = this.getRoute("auth");
1214
+ const res = await this.fetch(
1215
+ `${this.baseUrl}${route}/organization/list-teams?organizationId=${encodeURIComponent(organizationId)}`
1216
+ );
1217
+ const data = await res.json();
1218
+ const teams = Array.isArray(data) ? data : data?.data ?? data?.teams ?? [];
1219
+ return { teams };
1220
+ },
1221
+ /** better-auth: POST /organization/create-team */
1222
+ create: async (req) => {
1223
+ const route = this.getRoute("auth");
1224
+ const res = await this.fetch(`${this.baseUrl}${route}/organization/create-team`, {
1225
+ method: "POST",
1226
+ body: JSON.stringify(req)
1227
+ });
1228
+ return res.json();
1229
+ },
1230
+ /** better-auth: POST /organization/update-team */
1231
+ update: async (params) => {
1232
+ const route = this.getRoute("auth");
1233
+ const res = await this.fetch(`${this.baseUrl}${route}/organization/update-team`, {
1234
+ method: "POST",
1235
+ body: JSON.stringify(params)
1236
+ });
1237
+ return res.json();
1238
+ },
1239
+ /** better-auth: POST /organization/remove-team */
1240
+ delete: async (params) => {
1241
+ const route = this.getRoute("auth");
1242
+ const res = await this.fetch(`${this.baseUrl}${route}/organization/remove-team`, {
1243
+ method: "POST",
1244
+ body: JSON.stringify(params)
1245
+ });
1246
+ return res.json();
1247
+ },
1248
+ /** better-auth: GET /organization/list-team-members?teamId=… */
1249
+ listMembers: async (teamId) => {
1250
+ const route = this.getRoute("auth");
1251
+ const res = await this.fetch(
1252
+ `${this.baseUrl}${route}/organization/list-team-members?teamId=${encodeURIComponent(teamId)}`
1253
+ );
1254
+ const data = await res.json();
1255
+ const members = Array.isArray(data) ? data : data?.data ?? data?.members ?? [];
1256
+ return { members };
1257
+ },
1258
+ /** better-auth: POST /organization/add-team-member */
1259
+ addMember: async (params) => {
1260
+ const route = this.getRoute("auth");
1261
+ const res = await this.fetch(`${this.baseUrl}${route}/organization/add-team-member`, {
1262
+ method: "POST",
1263
+ body: JSON.stringify(params)
1264
+ });
1265
+ return res.json();
1266
+ },
1267
+ /** better-auth: POST /organization/remove-team-member */
1268
+ removeMember: async (params) => {
1269
+ const route = this.getRoute("auth");
1270
+ const res = await this.fetch(`${this.baseUrl}${route}/organization/remove-team-member`, {
1271
+ method: "POST",
1272
+ body: JSON.stringify(params)
1273
+ });
1274
+ return res.json();
1275
+ }
1276
+ }
1277
+ };
1278
+ /**
1279
+ * OAuth / OpenID Connect Provider — admin endpoints exposed by
1280
+ * `@better-auth/oauth-provider` (when enabled on the server). Lets users
1281
+ * register their own OAuth client applications, list them, and revoke them.
1282
+ *
1283
+ * All endpoints are mounted under the auth route, e.g. `/api/v1/auth/oauth2/*`.
1284
+ */
1285
+ this.oauth = {
1286
+ applications: {
1287
+ /**
1288
+ * Register a new OAuth client application.
1289
+ * POST /api/v1/auth/oauth2/create-client (authenticated)
1290
+ *
1291
+ * Returns the freshly-issued `client_id` and `client_secret`.
1292
+ * The secret is only returned at creation time — store it securely.
1293
+ */
1294
+ register: async (req) => {
1295
+ const route = this.getRoute("auth");
1296
+ const res = await this.fetch(`${this.baseUrl}${route}/oauth2/create-client`, {
1297
+ method: "POST",
1298
+ body: JSON.stringify(req)
1299
+ });
1300
+ return res.json();
1301
+ },
1302
+ /**
1303
+ * Get a single OAuth application by its `client_id`.
1304
+ * GET /api/v1/auth/oauth2/get-client?client_id=...
1305
+ */
1306
+ get: async (clientId) => {
1307
+ const route = this.getRoute("auth");
1308
+ const res = await this.fetch(
1309
+ `${this.baseUrl}${route}/oauth2/get-client?client_id=${encodeURIComponent(clientId)}`
1310
+ );
1311
+ return res.json();
1312
+ },
1313
+ /**
1314
+ * Get a single OAuth application's public fields (no auth required
1315
+ * once the user has signed in). Used by the consent screen.
1316
+ * GET /api/v1/auth/oauth2/public-client?client_id=...
1317
+ */
1318
+ getPublic: async (clientId) => {
1319
+ const route = this.getRoute("auth");
1320
+ const res = await this.fetch(
1321
+ `${this.baseUrl}${route}/oauth2/public-client?client_id=${encodeURIComponent(clientId)}`
1322
+ );
1323
+ return res.json();
1324
+ },
1325
+ /**
1326
+ * List OAuth applications visible to the current user.
1327
+ *
1328
+ * Uses `@better-auth/oauth-provider`'s `/oauth2/get-clients` endpoint
1329
+ * which returns clients owned by the current user (and their
1330
+ * organization, if applicable).
1331
+ */
1332
+ list: async () => {
1333
+ const route = this.getRoute("auth");
1334
+ const res = await this.fetch(`${this.baseUrl}${route}/oauth2/get-clients`);
1335
+ const data = await res.json();
1336
+ const items = Array.isArray(data) ? data : data?.clients ?? data?.data ?? [];
1337
+ return { applications: items };
1338
+ },
1339
+ /**
1340
+ * Delete an OAuth application by its `client_id`.
1341
+ * POST /api/v1/auth/oauth2/delete-client
1342
+ *
1343
+ * Tokens and consents referencing the client cascade-delete via the
1344
+ * better-auth schema's `onDelete: cascade` foreign keys.
1345
+ */
1346
+ delete: async (clientId) => {
1347
+ const route = this.getRoute("auth");
1348
+ const res = await this.fetch(`${this.baseUrl}${route}/oauth2/delete-client`, {
1349
+ method: "POST",
1350
+ body: JSON.stringify({ client_id: clientId })
1351
+ });
1352
+ return res.json();
1353
+ }
1354
+ },
1355
+ /**
1356
+ * Submit the user's decision to a pending consent request.
1357
+ * POST /api/v1/auth/oauth2/consent
1358
+ *
1359
+ * Called by the consent screen after the user accepts or denies. The
1360
+ * `oauth_query` is the raw query string of the consent page URL — it
1361
+ * carries the signed authorization request that the consent endpoint
1362
+ * verifies before issuing the authorization code.
1363
+ */
1364
+ consent: async (req) => {
1365
+ const route = this.getRoute("auth");
1366
+ const res = await this.fetch(`${this.baseUrl}${route}/oauth2/consent`, {
1367
+ method: "POST",
1368
+ body: JSON.stringify(req)
1369
+ });
1370
+ return res.json();
1371
+ }
1372
+ };
634
1373
  /**
635
1374
  * Authentication Services
636
1375
  */
@@ -645,70 +1384,370 @@ var ObjectStackClient = class {
645
1384
  return this.unwrapResponse(res);
646
1385
  },
647
1386
  /**
648
- * Login with email and password
649
- * Uses better-auth endpoint: POST /sign-in/email
1387
+ * Login with email and password
1388
+ * Uses better-auth endpoint: POST /sign-in/email
1389
+ */
1390
+ login: async (request) => {
1391
+ const route = this.getRoute("auth");
1392
+ const res = await this.fetch(`${this.baseUrl}${route}/sign-in/email`, {
1393
+ method: "POST",
1394
+ headers: { Origin: this.baseUrl },
1395
+ body: JSON.stringify(request)
1396
+ });
1397
+ const raw = await res.json();
1398
+ const data = raw && (raw.data ?? (raw.token || raw.user ? { token: raw.token, user: raw.user } : void 0));
1399
+ const normalized = data ? { ...raw, data } : raw;
1400
+ if (normalized.data?.token) {
1401
+ this.token = normalized.data.token;
1402
+ }
1403
+ return normalized;
1404
+ },
1405
+ /**
1406
+ * Logout current user
1407
+ * Uses better-auth endpoint: POST /sign-out
1408
+ */
1409
+ logout: async () => {
1410
+ const route = this.getRoute("auth");
1411
+ await this.fetch(`${this.baseUrl}${route}/sign-out`, {
1412
+ method: "POST",
1413
+ headers: { "Content-Type": "application/json", Origin: this.baseUrl },
1414
+ body: "{}"
1415
+ });
1416
+ this.token = void 0;
1417
+ },
1418
+ /**
1419
+ * Get current user session
1420
+ * Uses better-auth endpoint: GET /get-session
1421
+ */
1422
+ me: async () => {
1423
+ const route = this.getRoute("auth");
1424
+ const res = await this.fetch(`${this.baseUrl}${route}/get-session`, {
1425
+ headers: { Origin: this.baseUrl }
1426
+ });
1427
+ return res.json();
1428
+ },
1429
+ /**
1430
+ * Register a new user account
1431
+ * Uses better-auth endpoint: POST /sign-up/email
1432
+ */
1433
+ register: async (request) => {
1434
+ const route = this.getRoute("auth");
1435
+ const res = await this.fetch(`${this.baseUrl}${route}/sign-up/email`, {
1436
+ method: "POST",
1437
+ headers: { Origin: this.baseUrl },
1438
+ body: JSON.stringify(request)
1439
+ });
1440
+ const raw = await res.json();
1441
+ const data = raw && (raw.data ?? (raw.token || raw.user ? { token: raw.token, user: raw.user } : void 0));
1442
+ const normalized = data ? { ...raw, data } : raw;
1443
+ if (normalized.data?.token) {
1444
+ this.token = normalized.data.token;
1445
+ }
1446
+ return normalized;
1447
+ },
1448
+ /**
1449
+ * Initiate OAuth sign-in via a social or OIDC provider.
1450
+ *
1451
+ * - Social providers (Google, GitHub, etc.): calls POST /sign-in/social with `{ provider }`.
1452
+ * - OIDC/enterprise providers: calls POST /sign-in/oauth2 with `{ providerId }`.
1453
+ *
1454
+ * After the provider callback better-auth sets the session cookie and redirects to `callbackURL`.
1455
+ */
1456
+ signInWithProvider: async (provider, opts) => {
1457
+ if (typeof window === "undefined") {
1458
+ throw new Error("signInWithProvider requires a browser environment");
1459
+ }
1460
+ const route = this.getRoute("auth");
1461
+ const callbackURL = opts?.callbackURL ?? window.location.origin + "/login";
1462
+ const isOidc = opts?.type === "oidc";
1463
+ const endpoint = isOidc ? "/sign-in/oauth2" : "/sign-in/social";
1464
+ const body = isOidc ? { providerId: provider, callbackURL } : { provider, callbackURL };
1465
+ if (opts?.errorCallbackURL) body.errorCallbackURL = opts.errorCallbackURL;
1466
+ const res = await this.fetch(`${this.baseUrl}${route}${endpoint}`, {
1467
+ method: "POST",
1468
+ body: JSON.stringify(body)
1469
+ });
1470
+ const data = await res.json();
1471
+ const redirectUrl = data?.url ?? data?.data?.url;
1472
+ if (redirectUrl) {
1473
+ window.location.assign(redirectUrl);
1474
+ } else {
1475
+ throw new Error(`signInWithProvider: no redirect URL returned for provider "${provider}"`);
1476
+ }
1477
+ },
1478
+ /**
1479
+ * Refresh an authentication token
1480
+ * Note: better-auth handles token refresh automatically via /get-session
1481
+ * @param _refreshToken - Not used (better-auth handles refresh automatically)
1482
+ */
1483
+ refreshToken: async (_refreshToken) => {
1484
+ const route = this.getRoute("auth");
1485
+ const res = await this.fetch(`${this.baseUrl}${route}/get-session`, {
1486
+ method: "GET"
1487
+ });
1488
+ const data = await res.json();
1489
+ if (data.data?.token) {
1490
+ this.token = data.data.token;
1491
+ }
1492
+ return data;
1493
+ },
1494
+ /**
1495
+ * Probe the framework-only `/auth/bootstrap-status` endpoint to determine
1496
+ * whether the very first owner has been provisioned. The Account portal's
1497
+ * `/setup` route uses this to decide whether to render the bootstrap form
1498
+ * or bounce the user straight to `/login`.
1499
+ */
1500
+ bootstrapStatus: async () => {
1501
+ const route = this.getRoute("auth");
1502
+ const res = await this.fetch(`${this.baseUrl}${route}/bootstrap-status`);
1503
+ const data = await res.json();
1504
+ const payload = data?.data ?? data;
1505
+ return { hasOwner: !!payload?.hasOwner };
1506
+ },
1507
+ /**
1508
+ * Update the current user's profile.
1509
+ *
1510
+ * better-auth: POST /update-user — accepts `{ name?, image?, ... }`
1511
+ * (any custom user fields configured on the server). Returns the
1512
+ * updated user.
1513
+ */
1514
+ updateUser: async (data) => {
1515
+ const route = this.getRoute("auth");
1516
+ const res = await this.fetch(`${this.baseUrl}${route}/update-user`, {
1517
+ method: "POST",
1518
+ body: JSON.stringify(data)
1519
+ });
1520
+ return res.json();
1521
+ },
1522
+ /**
1523
+ * Change the current user's password (email/password accounts only).
1524
+ *
1525
+ * better-auth: POST /change-password.
1526
+ * Set `revokeOtherSessions: true` to invalidate every other session
1527
+ * after the change.
1528
+ */
1529
+ changePassword: async (req) => {
1530
+ const route = this.getRoute("auth");
1531
+ const res = await this.fetch(`${this.baseUrl}${route}/change-password`, {
1532
+ method: "POST",
1533
+ body: JSON.stringify(req)
1534
+ });
1535
+ return res.json();
1536
+ },
1537
+ /**
1538
+ * Begin a change-email flow. better-auth sends a verification mail to
1539
+ * the new address; the change only takes effect after the user clicks
1540
+ * the link.
1541
+ *
1542
+ * better-auth: POST /change-email — `{ newEmail, callbackURL? }`.
650
1543
  */
651
- login: async (request) => {
1544
+ changeEmail: async (req) => {
652
1545
  const route = this.getRoute("auth");
653
- const res = await this.fetch(`${this.baseUrl}${route}/sign-in/email`, {
1546
+ const res = await this.fetch(`${this.baseUrl}${route}/change-email`, {
654
1547
  method: "POST",
655
- body: JSON.stringify(request)
1548
+ body: JSON.stringify(req)
656
1549
  });
657
- const data = await res.json();
658
- if (data.data?.token) {
659
- this.token = data.data.token;
660
- }
661
- return data;
1550
+ return res.json();
662
1551
  },
663
1552
  /**
664
- * Logout current user
665
- * Uses better-auth endpoint: POST /sign-out
1553
+ * Re-send the email-verification link to the current user (or any
1554
+ * address when called as an admin). better-auth: POST /send-verification-email.
666
1555
  */
667
- logout: async () => {
1556
+ sendVerificationEmail: async (req) => {
668
1557
  const route = this.getRoute("auth");
669
- await this.fetch(`${this.baseUrl}${route}/sign-out`, { method: "POST" });
670
- this.token = void 0;
1558
+ const res = await this.fetch(`${this.baseUrl}${route}/send-verification-email`, {
1559
+ method: "POST",
1560
+ body: JSON.stringify(req)
1561
+ });
1562
+ return res.json();
671
1563
  },
672
1564
  /**
673
- * Get current user session
674
- * Uses better-auth endpoint: GET /get-session
1565
+ * Verify an email-verification token (the link target).
1566
+ *
1567
+ * better-auth: GET /verify-email?token=…&callbackURL=…
675
1568
  */
676
- me: async () => {
1569
+ verifyEmail: async (params) => {
677
1570
  const route = this.getRoute("auth");
678
- const res = await this.fetch(`${this.baseUrl}${route}/get-session`);
1571
+ const url = new URL(`${this.baseUrl}${route}/verify-email`);
1572
+ url.searchParams.set("token", params.token);
1573
+ if (params.callbackURL) url.searchParams.set("callbackURL", params.callbackURL);
1574
+ const res = await this.fetch(url.toString());
679
1575
  return res.json();
680
1576
  },
681
1577
  /**
682
- * Register a new user account
683
- * Uses better-auth endpoint: POST /sign-up/email
1578
+ * Permanently delete the current user. better-auth supports two flows:
1579
+ *
1580
+ * 1. With a fresh-session password challenge: POST `{ password }`.
1581
+ * 2. With an emailed deletion-confirmation token: POST `{ token }`,
1582
+ * typically following an out-of-band confirmation step.
1583
+ *
1584
+ * Server policy decides which is required; pass whichever you have.
684
1585
  */
685
- register: async (request) => {
1586
+ deleteUser: async (req) => {
686
1587
  const route = this.getRoute("auth");
687
- const res = await this.fetch(`${this.baseUrl}${route}/sign-up/email`, {
1588
+ const res = await this.fetch(`${this.baseUrl}${route}/delete-user`, {
688
1589
  method: "POST",
689
- body: JSON.stringify(request)
1590
+ body: JSON.stringify(req)
690
1591
  });
691
- const data = await res.json();
692
- if (data.data?.token) {
693
- this.token = data.data.token;
1592
+ this.token = void 0;
1593
+ return res.json();
1594
+ },
1595
+ /**
1596
+ * Active-session management. Wraps better-auth's session endpoints so
1597
+ * the Account portal's `/account/sessions` page can list every device
1598
+ * the user is signed in from and revoke them individually or in bulk.
1599
+ */
1600
+ sessions: {
1601
+ /** better-auth: GET /list-sessions — returns the current user's sessions. */
1602
+ list: async () => {
1603
+ const route = this.getRoute("auth");
1604
+ const res = await this.fetch(`${this.baseUrl}${route}/list-sessions`);
1605
+ const data = await res.json();
1606
+ const sessions = Array.isArray(data) ? data : data?.data ?? data?.sessions ?? [];
1607
+ return { sessions };
1608
+ },
1609
+ /** better-auth: POST /revoke-session — revoke a single session by token. */
1610
+ revoke: async (token) => {
1611
+ const route = this.getRoute("auth");
1612
+ const res = await this.fetch(`${this.baseUrl}${route}/revoke-session`, {
1613
+ method: "POST",
1614
+ body: JSON.stringify({ token })
1615
+ });
1616
+ return res.json();
1617
+ },
1618
+ /** better-auth: POST /revoke-other-sessions — keep current, kill the rest. */
1619
+ revokeOthers: async () => {
1620
+ const route = this.getRoute("auth");
1621
+ const res = await this.fetch(`${this.baseUrl}${route}/revoke-other-sessions`, {
1622
+ method: "POST",
1623
+ body: "{}"
1624
+ });
1625
+ return res.json();
1626
+ },
1627
+ /** better-auth: POST /revoke-sessions — kill every session for this user. */
1628
+ revokeAll: async () => {
1629
+ const route = this.getRoute("auth");
1630
+ const res = await this.fetch(`${this.baseUrl}${route}/revoke-sessions`, {
1631
+ method: "POST",
1632
+ body: "{}"
1633
+ });
1634
+ this.token = void 0;
1635
+ return res.json();
694
1636
  }
695
- return data;
696
1637
  },
697
1638
  /**
698
- * Refresh an authentication token
699
- * Note: better-auth handles token refresh automatically via /get-session
700
- * @param _refreshToken - Not used (better-auth handles refresh automatically)
1639
+ * Two-factor authentication (TOTP + backup codes). Requires the
1640
+ * `twoFactor` plugin to be enabled on the server (see
1641
+ * `plugin-auth` config). Endpoints live under `/two-factor/*`.
701
1642
  */
702
- refreshToken: async (_refreshToken) => {
703
- const route = this.getRoute("auth");
704
- const res = await this.fetch(`${this.baseUrl}${route}/get-session`, {
705
- method: "GET"
706
- });
707
- const data = await res.json();
708
- if (data.data?.token) {
709
- this.token = data.data.token;
1643
+ twoFactor: {
1644
+ /**
1645
+ * Start enrolment. Server returns a TOTP URI (`otpauth://...`) which
1646
+ * the UI renders as a QR code; the user then calls `verifyTotp` to
1647
+ * confirm and finish enabling.
1648
+ */
1649
+ enable: async (req) => {
1650
+ const route = this.getRoute("auth");
1651
+ const res = await this.fetch(`${this.baseUrl}${route}/two-factor/enable`, {
1652
+ method: "POST",
1653
+ body: JSON.stringify(req)
1654
+ });
1655
+ const data = await res.json();
1656
+ return data?.data ?? data;
1657
+ },
1658
+ /**
1659
+ * Confirm a TOTP code — used to finalise enrolment after `enable()`
1660
+ * or to step up an existing 2FA-enabled session. `trustDevice` (when
1661
+ * supported by the server config) suppresses the 2FA challenge on
1662
+ * this browser for the configured trust period.
1663
+ */
1664
+ verifyTotp: async (req) => {
1665
+ const route = this.getRoute("auth");
1666
+ const res = await this.fetch(`${this.baseUrl}${route}/two-factor/verify-totp`, {
1667
+ method: "POST",
1668
+ body: JSON.stringify(req)
1669
+ });
1670
+ return res.json();
1671
+ },
1672
+ /** Disable 2FA for the current user. Requires the password again. */
1673
+ disable: async (req) => {
1674
+ const route = this.getRoute("auth");
1675
+ const res = await this.fetch(`${this.baseUrl}${route}/two-factor/disable`, {
1676
+ method: "POST",
1677
+ body: JSON.stringify(req)
1678
+ });
1679
+ return res.json();
1680
+ },
1681
+ /**
1682
+ * Issue a fresh set of backup codes (invalidating any previous set).
1683
+ * Display them once — the server only stores hashes.
1684
+ */
1685
+ generateBackupCodes: async (req) => {
1686
+ const route = this.getRoute("auth");
1687
+ const res = await this.fetch(`${this.baseUrl}${route}/two-factor/generate-backup-codes`, {
1688
+ method: "POST",
1689
+ body: JSON.stringify(req)
1690
+ });
1691
+ const data = await res.json();
1692
+ return data?.data ?? data;
1693
+ },
1694
+ /**
1695
+ * Verify a 2FA backup code in lieu of a TOTP. Useful as a recovery
1696
+ * affordance when the user has lost their authenticator app.
1697
+ */
1698
+ verifyBackupCode: async (req) => {
1699
+ const route = this.getRoute("auth");
1700
+ const res = await this.fetch(`${this.baseUrl}${route}/two-factor/verify-backup-code`, {
1701
+ method: "POST",
1702
+ body: JSON.stringify(req)
1703
+ });
1704
+ return res.json();
1705
+ }
1706
+ },
1707
+ /**
1708
+ * Linked credentials — i.e. the rows in better-auth's `account` table
1709
+ * (one per provider × user). Lets the user see and unlink their social
1710
+ * / OIDC connections from the Account portal.
1711
+ */
1712
+ accounts: {
1713
+ /** better-auth: GET /list-accounts */
1714
+ list: async () => {
1715
+ const route = this.getRoute("auth");
1716
+ const res = await this.fetch(`${this.baseUrl}${route}/list-accounts`);
1717
+ const data = await res.json();
1718
+ const accounts = Array.isArray(data) ? data : data?.data ?? data?.accounts ?? [];
1719
+ return { accounts };
1720
+ },
1721
+ /**
1722
+ * Unlink a provider connection.
1723
+ * better-auth: POST /unlink-account — `{ providerId, accountId? }`.
1724
+ * `accountId` is required when the user has more than one account
1725
+ * for the same provider.
1726
+ */
1727
+ unlink: async (req) => {
1728
+ const route = this.getRoute("auth");
1729
+ const res = await this.fetch(`${this.baseUrl}${route}/unlink-account`, {
1730
+ method: "POST",
1731
+ body: JSON.stringify(req)
1732
+ });
1733
+ return res.json();
1734
+ },
1735
+ /**
1736
+ * Link an additional social provider to the current user.
1737
+ * better-auth: POST /link-social — `{ provider, callbackURL }`. The
1738
+ * server returns a redirect URL; the caller should `window.location`
1739
+ * to it (mirroring `signInWithProvider`).
1740
+ */
1741
+ linkSocial: async (req) => {
1742
+ const route = this.getRoute("auth");
1743
+ const callbackURL = req.callbackURL ?? (typeof window !== "undefined" ? window.location.href : void 0);
1744
+ const res = await this.fetch(`${this.baseUrl}${route}/link-social`, {
1745
+ method: "POST",
1746
+ body: JSON.stringify({ provider: req.provider, callbackURL })
1747
+ });
1748
+ const data = await res.json();
1749
+ return data?.data ?? data;
710
1750
  }
711
- return data;
712
1751
  }
713
1752
  };
714
1753
  /**
@@ -915,6 +1954,46 @@ var ObjectStackClient = class {
915
1954
  const res = await this.fetch(`${this.baseUrl}${route}/${flowName}/runs/${runId}`);
916
1955
  return this.unwrapResponse(res);
917
1956
  }
1957
+ },
1958
+ /**
1959
+ * Flat aliases mirroring the ScopedProjectClient.automation surface so
1960
+ * Studio (and other consumers) can use the same call shape regardless of
1961
+ * whether they hold a scoped or unscoped client.
1962
+ */
1963
+ /** Alias for `automation.get` — fetch a flow definition by name. */
1964
+ getFlow: async (name) => {
1965
+ const route = this.getRoute("automation");
1966
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(name)}`);
1967
+ return this.unwrapResponse(res);
1968
+ },
1969
+ /** Execute (trigger) a flow with an execution context. */
1970
+ execute: async (name, ctx) => {
1971
+ const route = this.getRoute("automation");
1972
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(name)}/trigger`, {
1973
+ method: "POST",
1974
+ body: JSON.stringify(ctx ?? {})
1975
+ });
1976
+ return this.unwrapResponse(res);
1977
+ },
1978
+ /** Alias for `automation.runs.list`. */
1979
+ listRuns: async (flowName, opts) => {
1980
+ const route = this.getRoute("automation");
1981
+ const params = new URLSearchParams();
1982
+ if (opts?.limit != null) params.set("limit", String(opts.limit));
1983
+ if (opts?.cursor) params.set("cursor", opts.cursor);
1984
+ const qs = params.toString();
1985
+ const res = await this.fetch(
1986
+ `${this.baseUrl}${route}/${encodeURIComponent(flowName)}/runs${qs ? `?${qs}` : ""}`
1987
+ );
1988
+ return this.unwrapResponse(res);
1989
+ },
1990
+ /** Alias for `automation.runs.get`. */
1991
+ getRun: async (flowName, runId) => {
1992
+ const route = this.getRoute("automation");
1993
+ const res = await this.fetch(
1994
+ `${this.baseUrl}${route}/${encodeURIComponent(flowName)}/runs/${encodeURIComponent(runId)}`
1995
+ );
1996
+ return this.unwrapResponse(res);
918
1997
  }
919
1998
  };
920
1999
  /**
@@ -1605,6 +2684,7 @@ var ObjectStackClient = class {
1605
2684
  };
1606
2685
  this.baseUrl = config.baseUrl.replace(/\/$/, "");
1607
2686
  this.token = config.token;
2687
+ this.projectId = config.projectId;
1608
2688
  this.fetchImpl = config.fetch || globalThis.fetch.bind(globalThis);
1609
2689
  this.logger = config.logger || createLogger({
1610
2690
  level: config.debug ? "debug" : "info",
@@ -1681,6 +2761,64 @@ var ObjectStackClient = class {
1681
2761
  }
1682
2762
  return result;
1683
2763
  }
2764
+ /**
2765
+ * Project-scoped client factory.
2766
+ *
2767
+ * Returns a thin wrapper around the data / meta / packages namespaces that
2768
+ * prefixes every request with `/api/v1/projects/:projectId/...`. Use this
2769
+ * when the server has `enableProjectScoping: true` in its REST API config.
2770
+ *
2771
+ * Backward compatibility: `client.data.*`, `client.meta.*`, and
2772
+ * `client.packages.*` continue to work unchanged; they hit unscoped routes
2773
+ * and rely on hostname / `X-Project-Id` header / session resolution.
2774
+ *
2775
+ * @example
2776
+ * ```ts
2777
+ * const scoped = client.project('00000000-0000-0000-0000-000000000001');
2778
+ * const tasks = await scoped.data.find('task', { top: 10 });
2779
+ * const objects = await scoped.meta.getItems('object');
2780
+ * ```
2781
+ */
2782
+ project(projectId) {
2783
+ if (!projectId) {
2784
+ throw new Error("[ObjectStack] project(id): projectId is required");
2785
+ }
2786
+ return new ScopedProjectClient(this, projectId);
2787
+ }
2788
+ // ── Internal accessors exposed to ScopedProjectClient ────────────────
2789
+ // The scoped client lives in the same module so using module-level access
2790
+ // works; TypeScript requires these to be accessible, so we expose them via
2791
+ // small protected getters that keep the public surface unchanged.
2792
+ /** @internal */
2793
+ _baseUrl() {
2794
+ return this.baseUrl;
2795
+ }
2796
+ /** @internal */
2797
+ _fetch(url, init) {
2798
+ return this.fetch(url, init);
2799
+ }
2800
+ /** @internal */
2801
+ _unwrap(res) {
2802
+ return this.unwrapResponse(res);
2803
+ }
2804
+ /** @internal */
2805
+ _isFilterAST(v) {
2806
+ return this.isFilterAST(v);
2807
+ }
2808
+ /**
2809
+ * Update the active project id used for subsequent requests.
2810
+ * Pass `undefined` to clear (falls back to the session default).
2811
+ */
2812
+ setProjectId(projectId) {
2813
+ this.projectId = projectId;
2814
+ this.logger.debug("Active project changed", { projectId });
2815
+ }
2816
+ /**
2817
+ * Current active project id (if set).
2818
+ */
2819
+ getProjectId() {
2820
+ return this.projectId;
2821
+ }
1684
2822
  /**
1685
2823
  * Event Subscription API
1686
2824
  * Provides real-time event subscriptions for metadata and data changes
@@ -1721,6 +2859,9 @@ var ObjectStackClient = class {
1721
2859
  if (this.token) {
1722
2860
  headers["Authorization"] = `Bearer ${this.token}`;
1723
2861
  }
2862
+ if (this.projectId) {
2863
+ headers["X-Project-Id"] = this.projectId;
2864
+ }
1724
2865
  const res = await this.fetchImpl(url, { ...options, headers });
1725
2866
  this.logger.debug("HTTP response", {
1726
2867
  method: options.method || "GET",
@@ -1787,11 +2928,246 @@ var ObjectStackClient = class {
1787
2928
  return routeMap[type] || `/api/v1/${type}`;
1788
2929
  }
1789
2930
  };
2931
+ var ScopedProjectClient = class {
2932
+ constructor(parent, projectId) {
2933
+ /**
2934
+ * Metadata operations scoped to this project.
2935
+ */
2936
+ this.meta = {
2937
+ getTypes: async () => {
2938
+ const res = await this.parent._fetch(this.url("/meta"));
2939
+ return this.parent._unwrap(res);
2940
+ },
2941
+ getItems: async (type, options) => {
2942
+ const params = new URLSearchParams();
2943
+ if (options?.packageId) params.set("package", options.packageId);
2944
+ const qs = params.toString();
2945
+ const res = await this.parent._fetch(this.url(`/meta/${type}${qs ? `?${qs}` : ""}`));
2946
+ return this.parent._unwrap(res);
2947
+ },
2948
+ getItem: async (type, name, options) => {
2949
+ const params = new URLSearchParams();
2950
+ if (options?.packageId) params.set("package", options.packageId);
2951
+ const qs = params.toString();
2952
+ const res = await this.parent._fetch(this.url(`/meta/${type}/${name}${qs ? `?${qs}` : ""}`));
2953
+ return this.parent._unwrap(res);
2954
+ },
2955
+ saveItem: async (type, name, item) => {
2956
+ const res = await this.parent._fetch(this.url(`/meta/${type}/${name}`), {
2957
+ method: "PUT",
2958
+ body: JSON.stringify(item)
2959
+ });
2960
+ return this.parent._unwrap(res);
2961
+ },
2962
+ deleteItem: async (type, name) => {
2963
+ const res = await this.parent._fetch(this.url(`/meta/${encodeURIComponent(type)}/${encodeURIComponent(name)}`), {
2964
+ method: "DELETE"
2965
+ });
2966
+ return this.parent._unwrap(res);
2967
+ }
2968
+ };
2969
+ /**
2970
+ * Data operations scoped to this project.
2971
+ *
2972
+ * Mirrors the query / find / get / create / update / delete / batch
2973
+ * surface on {@link ObjectStackClient}. URL construction differs only
2974
+ * in the prefix — query parameter serialization is identical.
2975
+ */
2976
+ this.data = {
2977
+ query: async (object, query) => {
2978
+ const res = await this.parent._fetch(this.url(`/data/${object}/query`), {
2979
+ method: "POST",
2980
+ body: JSON.stringify(query)
2981
+ });
2982
+ return this.parent._unwrap(res);
2983
+ },
2984
+ find: async (object, options = {}) => {
2985
+ const queryParams = new URLSearchParams();
2986
+ const v2 = options;
2987
+ const normalizedOptions = {};
2988
+ if ("where" in options || "fields" in options || "orderBy" in options || "offset" in options) {
2989
+ if (v2.where) normalizedOptions.filter = v2.where;
2990
+ if (v2.fields) normalizedOptions.select = v2.fields;
2991
+ if (v2.orderBy) normalizedOptions.sort = v2.orderBy;
2992
+ if (v2.limit != null) normalizedOptions.top = v2.limit;
2993
+ if (v2.offset != null) normalizedOptions.skip = v2.offset;
2994
+ if (v2.aggregations) normalizedOptions.aggregations = v2.aggregations;
2995
+ if (v2.groupBy) normalizedOptions.groupBy = v2.groupBy;
2996
+ } else {
2997
+ Object.assign(normalizedOptions, options);
2998
+ }
2999
+ if (normalizedOptions.top) queryParams.set("top", normalizedOptions.top.toString());
3000
+ if (normalizedOptions.skip) queryParams.set("skip", normalizedOptions.skip.toString());
3001
+ if (normalizedOptions.sort) {
3002
+ if (Array.isArray(normalizedOptions.sort) && typeof normalizedOptions.sort[0] === "object") {
3003
+ queryParams.set("sort", JSON.stringify(normalizedOptions.sort));
3004
+ } else {
3005
+ const sortVal = Array.isArray(normalizedOptions.sort) ? normalizedOptions.sort.join(",") : normalizedOptions.sort;
3006
+ queryParams.set("sort", sortVal);
3007
+ }
3008
+ }
3009
+ if (normalizedOptions.select) {
3010
+ queryParams.set("select", normalizedOptions.select.join(","));
3011
+ }
3012
+ const filterValue = normalizedOptions.filter ?? normalizedOptions.filters;
3013
+ if (filterValue) {
3014
+ if (this.parent._isFilterAST(filterValue) || Array.isArray(filterValue)) {
3015
+ queryParams.set("filter", JSON.stringify(filterValue));
3016
+ } else if (typeof filterValue === "object" && filterValue !== null) {
3017
+ Object.entries(filterValue).forEach(([k, v]) => {
3018
+ if (v !== void 0 && v !== null) {
3019
+ queryParams.append(k, String(v));
3020
+ }
3021
+ });
3022
+ }
3023
+ }
3024
+ if (normalizedOptions.aggregations) {
3025
+ queryParams.set("aggregations", JSON.stringify(normalizedOptions.aggregations));
3026
+ }
3027
+ if (normalizedOptions.groupBy) {
3028
+ queryParams.set("groupBy", normalizedOptions.groupBy.join(","));
3029
+ }
3030
+ const qs = queryParams.toString();
3031
+ const res = await this.parent._fetch(this.url(`/data/${object}${qs ? `?${qs}` : ""}`));
3032
+ return this.parent._unwrap(res);
3033
+ },
3034
+ get: async (object, id) => {
3035
+ const res = await this.parent._fetch(this.url(`/data/${object}/${id}`));
3036
+ return this.parent._unwrap(res);
3037
+ },
3038
+ create: async (object, data) => {
3039
+ const res = await this.parent._fetch(this.url(`/data/${object}`), {
3040
+ method: "POST",
3041
+ body: JSON.stringify(data)
3042
+ });
3043
+ return this.parent._unwrap(res);
3044
+ },
3045
+ createMany: async (object, data) => {
3046
+ const res = await this.parent._fetch(this.url(`/data/${object}/createMany`), {
3047
+ method: "POST",
3048
+ body: JSON.stringify(data)
3049
+ });
3050
+ return this.parent._unwrap(res);
3051
+ },
3052
+ update: async (object, id, data) => {
3053
+ const res = await this.parent._fetch(this.url(`/data/${object}/${id}`), {
3054
+ method: "PATCH",
3055
+ body: JSON.stringify(data)
3056
+ });
3057
+ return this.parent._unwrap(res);
3058
+ },
3059
+ batch: async (object, request) => {
3060
+ const res = await this.parent._fetch(this.url(`/data/${object}/batch`), {
3061
+ method: "POST",
3062
+ body: JSON.stringify(request)
3063
+ });
3064
+ return this.parent._unwrap(res);
3065
+ },
3066
+ updateMany: async (object, records, options) => {
3067
+ const request = { records, options };
3068
+ const res = await this.parent._fetch(this.url(`/data/${object}/updateMany`), {
3069
+ method: "POST",
3070
+ body: JSON.stringify(request)
3071
+ });
3072
+ return this.parent._unwrap(res);
3073
+ },
3074
+ delete: async (object, id) => {
3075
+ const res = await this.parent._fetch(this.url(`/data/${object}/${id}`), {
3076
+ method: "DELETE"
3077
+ });
3078
+ return this.parent._unwrap(res);
3079
+ },
3080
+ deleteMany: async (object, ids, options) => {
3081
+ const request = { ids, options };
3082
+ const res = await this.parent._fetch(this.url(`/data/${object}/deleteMany`), {
3083
+ method: "POST",
3084
+ body: JSON.stringify(request)
3085
+ });
3086
+ return this.parent._unwrap(res);
3087
+ }
3088
+ };
3089
+ /**
3090
+ * Package management scoped to this project.
3091
+ * Only the read-path is exposed here — publish / delete remain on the
3092
+ * global `client.packages` namespace for now, pending dedicated per-project
3093
+ * package tests.
3094
+ */
3095
+ this.packages = {
3096
+ list: async () => {
3097
+ const res = await this.parent._fetch(this.url("/packages"));
3098
+ return this.parent._unwrap(res);
3099
+ },
3100
+ get: async (id, version) => {
3101
+ const qs = version ? `?version=${encodeURIComponent(version)}` : "";
3102
+ const res = await this.parent._fetch(this.url(`/packages/${encodeURIComponent(id)}${qs}`));
3103
+ return this.parent._unwrap(res);
3104
+ }
3105
+ };
3106
+ /**
3107
+ * Automation (Flow) operations scoped to this project.
3108
+ *
3109
+ * Thin wrapper around the dispatcher's automation routes, mounted under
3110
+ * `/api/v1/projects/:projectId/automation/...`. Surface mirrors the methods
3111
+ * needed by Studio's Flow viewer: read flow definition, execute (trigger),
3112
+ * list runs, fetch a single run.
3113
+ */
3114
+ this.automation = {
3115
+ /** Fetch a flow definition by name. */
3116
+ getFlow: async (name) => {
3117
+ const res = await this.parent._fetch(this.url(`/automation/${encodeURIComponent(name)}`));
3118
+ return this.parent._unwrap(res);
3119
+ },
3120
+ /**
3121
+ * Execute (trigger) a flow by name. The request body is forwarded as the
3122
+ * automation execution context (e.g. `{ params, trigger }`).
3123
+ */
3124
+ execute: async (name, ctx) => {
3125
+ const res = await this.parent._fetch(this.url(`/automation/${encodeURIComponent(name)}/trigger`), {
3126
+ method: "POST",
3127
+ body: JSON.stringify(ctx ?? {})
3128
+ });
3129
+ return this.parent._unwrap(res);
3130
+ },
3131
+ /** List recent runs for a flow. */
3132
+ listRuns: async (flowName, opts) => {
3133
+ const params = new URLSearchParams();
3134
+ if (opts?.limit != null) params.set("limit", String(opts.limit));
3135
+ if (opts?.cursor) params.set("cursor", opts.cursor);
3136
+ const qs = params.toString();
3137
+ const res = await this.parent._fetch(
3138
+ this.url(`/automation/${encodeURIComponent(flowName)}/runs${qs ? `?${qs}` : ""}`)
3139
+ );
3140
+ return this.parent._unwrap(res);
3141
+ },
3142
+ /** Fetch a single run (with step log) for a flow. */
3143
+ getRun: async (flowName, runId) => {
3144
+ const res = await this.parent._fetch(
3145
+ this.url(`/automation/${encodeURIComponent(flowName)}/runs/${encodeURIComponent(runId)}`)
3146
+ );
3147
+ return this.parent._unwrap(res);
3148
+ }
3149
+ };
3150
+ this.parent = parent;
3151
+ this.projectId = projectId;
3152
+ }
3153
+ /** The projectId this client is scoped to. */
3154
+ getProjectId() {
3155
+ return this.projectId;
3156
+ }
3157
+ /** Prefix segment inserted between the baseUrl and the resource path. */
3158
+ scope() {
3159
+ return `/api/v1/projects/${encodeURIComponent(this.projectId)}`;
3160
+ }
3161
+ url(suffix) {
3162
+ return `${this.parent._baseUrl()}${this.scope()}${suffix}`;
3163
+ }
3164
+ };
1790
3165
  export {
1791
3166
  FilterBuilder,
1792
3167
  ObjectStackClient,
1793
3168
  QueryBuilder,
1794
3169
  RealtimeAPI,
3170
+ ScopedProjectClient,
1795
3171
  createFilter,
1796
3172
  createQuery
1797
3173
  };