@objectstack/client 4.0.3 → 4.0.5
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.d.mts +870 -6
- package/dist/index.d.ts +870 -6
- package/dist/index.js +1311 -46
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1309 -45
- package/dist/index.mjs.map +1 -1
- package/package.json +38 -13
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -637
- package/CLIENT_SERVER_INTEGRATION_TESTS.md +0 -939
- package/CLIENT_SPEC_COMPLIANCE.md +0 -361
- package/src/client.feed.test.ts +0 -273
- package/src/client.hono.test.ts +0 -161
- package/src/client.msw.test.ts +0 -223
- package/src/client.test.ts +0 -891
- package/src/index.ts +0 -1875
- package/src/query-builder.ts +0 -337
- package/src/realtime-api.ts +0 -208
- package/tests/integration/01-discovery.test.ts +0 -68
- package/tests/integration/README.md +0 -72
- package/tsconfig.json +0 -11
- package/vitest.config.ts +0 -13
- package/vitest.integration.config.ts +0 -18
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,10 +631,637 @@ 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
|
+
* Retry provisioning for a project stuck in `failed` (or
|
|
741
|
+
* `provisioning`) state. The server re-runs the driver handshake; on
|
|
742
|
+
* success the project flips to `active`, on failure it stays
|
|
743
|
+
* `failed` with `metadata.provisioningError` updated.
|
|
744
|
+
*/
|
|
745
|
+
retryProvisioning: async (id) => {
|
|
746
|
+
const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(id)}/retry`, {
|
|
747
|
+
method: "POST"
|
|
748
|
+
});
|
|
749
|
+
return this.unwrapResponse(res);
|
|
750
|
+
},
|
|
751
|
+
/**
|
|
752
|
+
* List members of a project (per-project RBAC).
|
|
753
|
+
*/
|
|
754
|
+
listMembers: async (id) => {
|
|
755
|
+
const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(id)}/members`);
|
|
756
|
+
return this.unwrapResponse(res);
|
|
757
|
+
},
|
|
758
|
+
/**
|
|
759
|
+
* List ObjectQL drivers registered on the server. Useful for populating a
|
|
760
|
+
* driver selector when provisioning a new project (memory / turso /
|
|
761
|
+
* future sql drivers). Returned `name` is the short alias (e.g. `memory`,
|
|
762
|
+
* `turso`); `driverId` is the full FQN (e.g. `com.objectstack.driver.memory`).
|
|
763
|
+
*/
|
|
764
|
+
listDrivers: async () => {
|
|
765
|
+
const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/drivers`);
|
|
766
|
+
return this.unwrapResponse(res);
|
|
767
|
+
},
|
|
768
|
+
/**
|
|
769
|
+
* List available project templates. Templates are seeded into the project
|
|
770
|
+
* database once at provisioning time when `template_id` is supplied.
|
|
771
|
+
*/
|
|
772
|
+
listTemplates: async () => {
|
|
773
|
+
const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/templates`);
|
|
774
|
+
return this.unwrapResponse(res);
|
|
775
|
+
},
|
|
776
|
+
/**
|
|
777
|
+
* Per-project package installation management (Power Apps "solution" model).
|
|
778
|
+
* Install records are stored in the environment's own database.
|
|
779
|
+
*/
|
|
780
|
+
packages: {
|
|
781
|
+
/** List all packages installed in a specific project. */
|
|
782
|
+
list: async (envId) => {
|
|
783
|
+
const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(envId)}/packages`);
|
|
784
|
+
return this.unwrapResponse(res);
|
|
785
|
+
},
|
|
786
|
+
/** Install a package into the project. */
|
|
787
|
+
install: async (envId, body) => {
|
|
788
|
+
const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(envId)}/packages`, {
|
|
789
|
+
method: "POST",
|
|
790
|
+
body: JSON.stringify(body)
|
|
791
|
+
});
|
|
792
|
+
return this.unwrapResponse(res);
|
|
793
|
+
},
|
|
794
|
+
/** Get a single installation record. */
|
|
795
|
+
get: async (envId, pkgId) => {
|
|
796
|
+
const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(envId)}/packages/${encodeURIComponent(pkgId)}`);
|
|
797
|
+
return this.unwrapResponse(res);
|
|
798
|
+
},
|
|
799
|
+
/** Enable a previously disabled package. */
|
|
800
|
+
enable: async (envId, pkgId) => {
|
|
801
|
+
const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(envId)}/packages/${encodeURIComponent(pkgId)}/enable`, {
|
|
802
|
+
method: "PATCH"
|
|
803
|
+
});
|
|
804
|
+
return this.unwrapResponse(res);
|
|
805
|
+
},
|
|
806
|
+
/** Disable an installed package (metadata will not be loaded). */
|
|
807
|
+
disable: async (envId, pkgId) => {
|
|
808
|
+
const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(envId)}/packages/${encodeURIComponent(pkgId)}/disable`, {
|
|
809
|
+
method: "PATCH"
|
|
810
|
+
});
|
|
811
|
+
return this.unwrapResponse(res);
|
|
812
|
+
},
|
|
813
|
+
/** Uninstall a package from the project. Forbidden for scope=platform packages. */
|
|
814
|
+
uninstall: async (envId, pkgId) => {
|
|
815
|
+
const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(envId)}/packages/${encodeURIComponent(pkgId)}`, {
|
|
816
|
+
method: "DELETE"
|
|
817
|
+
});
|
|
818
|
+
return this.unwrapResponse(res);
|
|
819
|
+
},
|
|
820
|
+
/** Upgrade an installed package to a newer version. */
|
|
821
|
+
upgrade: async (envId, pkgId, targetVersion) => {
|
|
822
|
+
const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/projects/${encodeURIComponent(envId)}/packages/${encodeURIComponent(pkgId)}/upgrade`, {
|
|
823
|
+
method: "POST",
|
|
824
|
+
body: JSON.stringify({ targetVersion })
|
|
825
|
+
});
|
|
826
|
+
return this.unwrapResponse(res);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
/**
|
|
831
|
+
* Organization Services
|
|
832
|
+
*
|
|
833
|
+
* Thin wrapper around better-auth's organization plugin endpoints, which
|
|
834
|
+
* are mounted under `/api/v1/auth/organization/**`. Used by the Studio
|
|
835
|
+
* OrganizationSwitcher and the /orgs management routes.
|
|
836
|
+
*/
|
|
837
|
+
this.organizations = {
|
|
838
|
+
/**
|
|
839
|
+
* List organizations the current user belongs to.
|
|
840
|
+
* GET /api/v1/auth/organization/list
|
|
841
|
+
*/
|
|
842
|
+
list: async () => {
|
|
843
|
+
const route = this.getRoute("auth");
|
|
844
|
+
const res = await this.fetch(`${this.baseUrl}${route}/organization/list`);
|
|
845
|
+
const data = await res.json();
|
|
846
|
+
const orgs = Array.isArray(data) ? data : data?.data ?? [];
|
|
847
|
+
return { organizations: orgs };
|
|
848
|
+
},
|
|
849
|
+
/**
|
|
850
|
+
* Create a new organization.
|
|
851
|
+
* POST /api/v1/auth/organization/create
|
|
852
|
+
*/
|
|
853
|
+
create: async (req) => {
|
|
854
|
+
const route = this.getRoute("auth");
|
|
855
|
+
const res = await this.fetch(`${this.baseUrl}${route}/organization/create`, {
|
|
856
|
+
method: "POST",
|
|
857
|
+
body: JSON.stringify(req)
|
|
858
|
+
});
|
|
859
|
+
return res.json();
|
|
860
|
+
},
|
|
861
|
+
/**
|
|
862
|
+
* Update an existing organization.
|
|
863
|
+
* POST /api/v1/auth/organization/update
|
|
864
|
+
*
|
|
865
|
+
* better-auth requires the caller to be an owner/admin (server-side
|
|
866
|
+
* enforcement); the body shape is `{ organizationId, data: {...} }`.
|
|
867
|
+
*/
|
|
868
|
+
update: async (organizationId, data) => {
|
|
869
|
+
const route = this.getRoute("auth");
|
|
870
|
+
const res = await this.fetch(`${this.baseUrl}${route}/organization/update`, {
|
|
871
|
+
method: "POST",
|
|
872
|
+
body: JSON.stringify({ organizationId, data })
|
|
873
|
+
});
|
|
874
|
+
return res.json();
|
|
875
|
+
},
|
|
876
|
+
/**
|
|
877
|
+
* Set the active organization on the current session. The server writes
|
|
878
|
+
* `activeOrganizationId` on the better-auth session, which downstream
|
|
879
|
+
* handlers (e.g. `EnvironmentProvisioningService`) consult.
|
|
880
|
+
*
|
|
881
|
+
* POST /api/v1/auth/organization/set-active
|
|
882
|
+
*/
|
|
883
|
+
setActive: async (organizationId) => {
|
|
884
|
+
const route = this.getRoute("auth");
|
|
885
|
+
const res = await this.fetch(`${this.baseUrl}${route}/organization/set-active`, {
|
|
886
|
+
method: "POST",
|
|
887
|
+
body: JSON.stringify({ organizationId })
|
|
888
|
+
});
|
|
889
|
+
return res.json();
|
|
890
|
+
},
|
|
891
|
+
/**
|
|
892
|
+
* Get full organization detail (members, invitations, teams).
|
|
893
|
+
* GET /api/v1/auth/organization/get-full-organization?organizationId=...
|
|
894
|
+
*/
|
|
895
|
+
get: async (organizationId) => {
|
|
896
|
+
const route = this.getRoute("auth");
|
|
897
|
+
const res = await this.fetch(
|
|
898
|
+
`${this.baseUrl}${route}/organization/get-full-organization?organizationId=${encodeURIComponent(organizationId)}`
|
|
899
|
+
);
|
|
900
|
+
return res.json();
|
|
901
|
+
},
|
|
902
|
+
/**
|
|
903
|
+
* List members of an organization.
|
|
904
|
+
*/
|
|
905
|
+
listMembers: async (organizationId) => {
|
|
906
|
+
const route = this.getRoute("auth");
|
|
907
|
+
const res = await this.fetch(
|
|
908
|
+
`${this.baseUrl}${route}/organization/list-members?organizationId=${encodeURIComponent(organizationId)}`
|
|
909
|
+
);
|
|
910
|
+
return res.json();
|
|
911
|
+
},
|
|
912
|
+
/**
|
|
913
|
+
* Invite a user to the organization.
|
|
914
|
+
*/
|
|
915
|
+
invite: async (req) => {
|
|
916
|
+
const route = this.getRoute("auth");
|
|
917
|
+
const res = await this.fetch(`${this.baseUrl}${route}/organization/invite-member`, {
|
|
918
|
+
method: "POST",
|
|
919
|
+
body: JSON.stringify(req)
|
|
920
|
+
});
|
|
921
|
+
return res.json();
|
|
922
|
+
},
|
|
923
|
+
/**
|
|
924
|
+
* Leave the given organization.
|
|
925
|
+
*/
|
|
926
|
+
leave: async (organizationId) => {
|
|
927
|
+
const route = this.getRoute("auth");
|
|
928
|
+
const res = await this.fetch(`${this.baseUrl}${route}/organization/leave`, {
|
|
929
|
+
method: "POST",
|
|
930
|
+
body: JSON.stringify({ organizationId })
|
|
931
|
+
});
|
|
932
|
+
return res.json();
|
|
933
|
+
},
|
|
934
|
+
/**
|
|
935
|
+
* Delete an organization via better-auth's organization plugin.
|
|
936
|
+
*
|
|
937
|
+
* POST /api/v1/auth/organization/delete
|
|
938
|
+
*
|
|
939
|
+
* better-auth removes the organization row, all members, and all
|
|
940
|
+
* pending invitations. Project teardown (per-project DBs, etc.) is
|
|
941
|
+
* handled server-side by hooks attached to the organization plugin.
|
|
942
|
+
*/
|
|
943
|
+
delete: async (organizationId) => {
|
|
944
|
+
const route = this.getRoute("auth");
|
|
945
|
+
const res = await this.fetch(`${this.baseUrl}${route}/organization/delete`, {
|
|
946
|
+
method: "POST",
|
|
947
|
+
body: JSON.stringify({ organizationId })
|
|
948
|
+
});
|
|
949
|
+
return res.json();
|
|
950
|
+
},
|
|
951
|
+
/**
|
|
952
|
+
* Remove a member from an organization.
|
|
953
|
+
*
|
|
954
|
+
* better-auth: POST /organization/remove-member
|
|
955
|
+
* Body: `{ memberIdOrEmail, organizationId? }` — note the parameter is the
|
|
956
|
+
* **member id** (the row id from `member` table) or the user's email; it
|
|
957
|
+
* is *not* the bare `userId`. Server enforces owner/admin permission.
|
|
958
|
+
*/
|
|
959
|
+
removeMember: async (organizationId, params) => {
|
|
960
|
+
const route = this.getRoute("auth");
|
|
961
|
+
const res = await this.fetch(`${this.baseUrl}${route}/organization/remove-member`, {
|
|
962
|
+
method: "POST",
|
|
963
|
+
body: JSON.stringify({ memberIdOrEmail: params.memberIdOrEmail, organizationId })
|
|
964
|
+
});
|
|
965
|
+
return res.json();
|
|
966
|
+
},
|
|
967
|
+
/**
|
|
968
|
+
* Change a member's role in an organization (owner/admin only).
|
|
969
|
+
*
|
|
970
|
+
* better-auth: POST /organization/update-member-role
|
|
971
|
+
* Body: `{ memberId, role, organizationId? }`. The `memberId` is the
|
|
972
|
+
* `member` table row id (not user id). `role` is one of the configured
|
|
973
|
+
* organisation roles (default: `owner | admin | member`).
|
|
974
|
+
*/
|
|
975
|
+
updateMemberRole: async (organizationId, params) => {
|
|
976
|
+
const route = this.getRoute("auth");
|
|
977
|
+
const res = await this.fetch(`${this.baseUrl}${route}/organization/update-member-role`, {
|
|
978
|
+
method: "POST",
|
|
979
|
+
body: JSON.stringify({ memberId: params.memberId, role: params.role, organizationId })
|
|
980
|
+
});
|
|
981
|
+
return res.json();
|
|
982
|
+
},
|
|
983
|
+
/**
|
|
984
|
+
* Look up the calling user's membership row in the given organisation.
|
|
985
|
+
* Useful for permission checks on the client without having to scan the
|
|
986
|
+
* full member list.
|
|
987
|
+
*
|
|
988
|
+
* better-auth: GET /organization/get-active-member?organizationId=…
|
|
989
|
+
*/
|
|
990
|
+
getActiveMember: async (organizationId) => {
|
|
991
|
+
const route = this.getRoute("auth");
|
|
992
|
+
const res = await this.fetch(
|
|
993
|
+
`${this.baseUrl}${route}/organization/get-active-member?organizationId=${encodeURIComponent(organizationId)}`
|
|
994
|
+
);
|
|
995
|
+
return res.json();
|
|
996
|
+
},
|
|
997
|
+
/**
|
|
998
|
+
* Invitation lifecycle — wraps better-auth's organization-plugin
|
|
999
|
+
* invitation endpoints. Always go through here instead of writing to
|
|
1000
|
+
* `sys_invitation` via the data API: the better-auth writers handle
|
|
1001
|
+
* status transitions, expiry, dedupe, and the `sendInvitationEmail`
|
|
1002
|
+
* side-effect that the auth-manager wires up.
|
|
1003
|
+
*/
|
|
1004
|
+
invitations: {
|
|
1005
|
+
/**
|
|
1006
|
+
* List pending/accepted/canceled invitations for an organization.
|
|
1007
|
+
* Requires owner/admin role on that org.
|
|
1008
|
+
*
|
|
1009
|
+
* better-auth: GET /organization/list-invitations?organizationId=…
|
|
1010
|
+
*/
|
|
1011
|
+
list: async (organizationId) => {
|
|
1012
|
+
const route = this.getRoute("auth");
|
|
1013
|
+
const res = await this.fetch(
|
|
1014
|
+
`${this.baseUrl}${route}/organization/list-invitations?organizationId=${encodeURIComponent(organizationId)}`
|
|
1015
|
+
);
|
|
1016
|
+
const data = await res.json();
|
|
1017
|
+
const invitations = Array.isArray(data) ? data : data?.data ?? data?.invitations ?? [];
|
|
1018
|
+
return { invitations };
|
|
1019
|
+
},
|
|
1020
|
+
/**
|
|
1021
|
+
* List the **current user's** incoming invitations across every
|
|
1022
|
+
* organisation. Used by the per-user "Invitations" inbox page.
|
|
1023
|
+
*
|
|
1024
|
+
* better-auth: GET /organization/list-user-invitations
|
|
1025
|
+
*/
|
|
1026
|
+
listMine: async () => {
|
|
1027
|
+
const route = this.getRoute("auth");
|
|
1028
|
+
const res = await this.fetch(`${this.baseUrl}${route}/organization/list-user-invitations`);
|
|
1029
|
+
const data = await res.json();
|
|
1030
|
+
const invitations = Array.isArray(data) ? data : data?.data ?? data?.invitations ?? [];
|
|
1031
|
+
return { invitations };
|
|
1032
|
+
},
|
|
1033
|
+
/** better-auth: POST /organization/cancel-invitation */
|
|
1034
|
+
cancel: async (invitationId) => {
|
|
1035
|
+
const route = this.getRoute("auth");
|
|
1036
|
+
const res = await this.fetch(`${this.baseUrl}${route}/organization/cancel-invitation`, {
|
|
1037
|
+
method: "POST",
|
|
1038
|
+
body: JSON.stringify({ invitationId })
|
|
1039
|
+
});
|
|
1040
|
+
return res.json();
|
|
1041
|
+
},
|
|
1042
|
+
/** better-auth: POST /organization/accept-invitation */
|
|
1043
|
+
accept: async (invitationId) => {
|
|
1044
|
+
const route = this.getRoute("auth");
|
|
1045
|
+
const res = await this.fetch(`${this.baseUrl}${route}/organization/accept-invitation`, {
|
|
1046
|
+
method: "POST",
|
|
1047
|
+
body: JSON.stringify({ invitationId })
|
|
1048
|
+
});
|
|
1049
|
+
return res.json();
|
|
1050
|
+
},
|
|
1051
|
+
/** better-auth: POST /organization/reject-invitation */
|
|
1052
|
+
reject: async (invitationId) => {
|
|
1053
|
+
const route = this.getRoute("auth");
|
|
1054
|
+
const res = await this.fetch(`${this.baseUrl}${route}/organization/reject-invitation`, {
|
|
1055
|
+
method: "POST",
|
|
1056
|
+
body: JSON.stringify({ invitationId })
|
|
1057
|
+
});
|
|
1058
|
+
return res.json();
|
|
1059
|
+
},
|
|
1060
|
+
/**
|
|
1061
|
+
* "Resend" an invitation. better-auth has no first-class resend
|
|
1062
|
+
* endpoint, so we implement it as cancel-then-invite: cancel the old
|
|
1063
|
+
* row (so its status flips to `canceled` and audit hooks fire), then
|
|
1064
|
+
* issue a fresh invite. The new invite re-runs `sendInvitationEmail`
|
|
1065
|
+
* on the server, so the recipient gets a brand-new accept URL.
|
|
1066
|
+
*
|
|
1067
|
+
* If `cancel()` fails (e.g. invite already accepted) the error is
|
|
1068
|
+
* re-thrown without re-inviting.
|
|
1069
|
+
*/
|
|
1070
|
+
resend: async (invitation) => {
|
|
1071
|
+
if (invitation.id) {
|
|
1072
|
+
try {
|
|
1073
|
+
await this.organizations.invitations.cancel(invitation.id);
|
|
1074
|
+
} catch {
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
return this.organizations.invite({
|
|
1078
|
+
email: invitation.email,
|
|
1079
|
+
role: invitation.role ?? "member",
|
|
1080
|
+
organizationId: invitation.organizationId
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
},
|
|
1084
|
+
/**
|
|
1085
|
+
* Team management — only available when the organisation plugin is
|
|
1086
|
+
* configured with `teams: { enabled: true }` on the server. Calls return
|
|
1087
|
+
* a 4xx if teams aren't enabled; UI should hide the section in that case.
|
|
1088
|
+
*/
|
|
1089
|
+
teams: {
|
|
1090
|
+
/** better-auth: GET /organization/list-teams?organizationId=… */
|
|
1091
|
+
list: async (organizationId) => {
|
|
1092
|
+
const route = this.getRoute("auth");
|
|
1093
|
+
const res = await this.fetch(
|
|
1094
|
+
`${this.baseUrl}${route}/organization/list-teams?organizationId=${encodeURIComponent(organizationId)}`
|
|
1095
|
+
);
|
|
1096
|
+
const data = await res.json();
|
|
1097
|
+
const teams = Array.isArray(data) ? data : data?.data ?? data?.teams ?? [];
|
|
1098
|
+
return { teams };
|
|
1099
|
+
},
|
|
1100
|
+
/** better-auth: POST /organization/create-team */
|
|
1101
|
+
create: async (req) => {
|
|
1102
|
+
const route = this.getRoute("auth");
|
|
1103
|
+
const res = await this.fetch(`${this.baseUrl}${route}/organization/create-team`, {
|
|
1104
|
+
method: "POST",
|
|
1105
|
+
body: JSON.stringify(req)
|
|
1106
|
+
});
|
|
1107
|
+
return res.json();
|
|
1108
|
+
},
|
|
1109
|
+
/** better-auth: POST /organization/update-team */
|
|
1110
|
+
update: async (params) => {
|
|
1111
|
+
const route = this.getRoute("auth");
|
|
1112
|
+
const res = await this.fetch(`${this.baseUrl}${route}/organization/update-team`, {
|
|
1113
|
+
method: "POST",
|
|
1114
|
+
body: JSON.stringify(params)
|
|
1115
|
+
});
|
|
1116
|
+
return res.json();
|
|
1117
|
+
},
|
|
1118
|
+
/** better-auth: POST /organization/remove-team */
|
|
1119
|
+
delete: async (params) => {
|
|
1120
|
+
const route = this.getRoute("auth");
|
|
1121
|
+
const res = await this.fetch(`${this.baseUrl}${route}/organization/remove-team`, {
|
|
1122
|
+
method: "POST",
|
|
1123
|
+
body: JSON.stringify(params)
|
|
1124
|
+
});
|
|
1125
|
+
return res.json();
|
|
1126
|
+
},
|
|
1127
|
+
/** better-auth: GET /organization/list-team-members?teamId=… */
|
|
1128
|
+
listMembers: async (teamId) => {
|
|
1129
|
+
const route = this.getRoute("auth");
|
|
1130
|
+
const res = await this.fetch(
|
|
1131
|
+
`${this.baseUrl}${route}/organization/list-team-members?teamId=${encodeURIComponent(teamId)}`
|
|
1132
|
+
);
|
|
1133
|
+
const data = await res.json();
|
|
1134
|
+
const members = Array.isArray(data) ? data : data?.data ?? data?.members ?? [];
|
|
1135
|
+
return { members };
|
|
1136
|
+
},
|
|
1137
|
+
/** better-auth: POST /organization/add-team-member */
|
|
1138
|
+
addMember: async (params) => {
|
|
1139
|
+
const route = this.getRoute("auth");
|
|
1140
|
+
const res = await this.fetch(`${this.baseUrl}${route}/organization/add-team-member`, {
|
|
1141
|
+
method: "POST",
|
|
1142
|
+
body: JSON.stringify(params)
|
|
1143
|
+
});
|
|
1144
|
+
return res.json();
|
|
1145
|
+
},
|
|
1146
|
+
/** better-auth: POST /organization/remove-team-member */
|
|
1147
|
+
removeMember: async (params) => {
|
|
1148
|
+
const route = this.getRoute("auth");
|
|
1149
|
+
const res = await this.fetch(`${this.baseUrl}${route}/organization/remove-team-member`, {
|
|
1150
|
+
method: "POST",
|
|
1151
|
+
body: JSON.stringify(params)
|
|
1152
|
+
});
|
|
1153
|
+
return res.json();
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
/**
|
|
1158
|
+
* OAuth / OpenID Connect Provider — admin endpoints exposed by
|
|
1159
|
+
* `@better-auth/oauth-provider` (when enabled on the server). Lets users
|
|
1160
|
+
* register their own OAuth client applications, list them, and revoke them.
|
|
1161
|
+
*
|
|
1162
|
+
* All endpoints are mounted under the auth route, e.g. `/api/v1/auth/oauth2/*`.
|
|
1163
|
+
*/
|
|
1164
|
+
this.oauth = {
|
|
1165
|
+
applications: {
|
|
1166
|
+
/**
|
|
1167
|
+
* Register a new OAuth client application.
|
|
1168
|
+
* POST /api/v1/auth/oauth2/create-client (authenticated)
|
|
1169
|
+
*
|
|
1170
|
+
* Returns the freshly-issued `client_id` and `client_secret`.
|
|
1171
|
+
* The secret is only returned at creation time — store it securely.
|
|
1172
|
+
*/
|
|
1173
|
+
register: async (req) => {
|
|
1174
|
+
const route = this.getRoute("auth");
|
|
1175
|
+
const res = await this.fetch(`${this.baseUrl}${route}/oauth2/create-client`, {
|
|
1176
|
+
method: "POST",
|
|
1177
|
+
body: JSON.stringify(req)
|
|
1178
|
+
});
|
|
1179
|
+
return res.json();
|
|
1180
|
+
},
|
|
1181
|
+
/**
|
|
1182
|
+
* Get a single OAuth application by its `client_id`.
|
|
1183
|
+
* GET /api/v1/auth/oauth2/get-client?client_id=...
|
|
1184
|
+
*/
|
|
1185
|
+
get: async (clientId) => {
|
|
1186
|
+
const route = this.getRoute("auth");
|
|
1187
|
+
const res = await this.fetch(
|
|
1188
|
+
`${this.baseUrl}${route}/oauth2/get-client?client_id=${encodeURIComponent(clientId)}`
|
|
1189
|
+
);
|
|
1190
|
+
return res.json();
|
|
1191
|
+
},
|
|
1192
|
+
/**
|
|
1193
|
+
* Get a single OAuth application's public fields (no auth required
|
|
1194
|
+
* once the user has signed in). Used by the consent screen.
|
|
1195
|
+
* GET /api/v1/auth/oauth2/public-client?client_id=...
|
|
1196
|
+
*/
|
|
1197
|
+
getPublic: async (clientId) => {
|
|
1198
|
+
const route = this.getRoute("auth");
|
|
1199
|
+
const res = await this.fetch(
|
|
1200
|
+
`${this.baseUrl}${route}/oauth2/public-client?client_id=${encodeURIComponent(clientId)}`
|
|
1201
|
+
);
|
|
1202
|
+
return res.json();
|
|
1203
|
+
},
|
|
1204
|
+
/**
|
|
1205
|
+
* List OAuth applications visible to the current user.
|
|
1206
|
+
*
|
|
1207
|
+
* Uses `@better-auth/oauth-provider`'s `/oauth2/get-clients` endpoint
|
|
1208
|
+
* which returns clients owned by the current user (and their
|
|
1209
|
+
* organization, if applicable).
|
|
1210
|
+
*/
|
|
1211
|
+
list: async () => {
|
|
1212
|
+
const route = this.getRoute("auth");
|
|
1213
|
+
const res = await this.fetch(`${this.baseUrl}${route}/oauth2/get-clients`);
|
|
1214
|
+
const data = await res.json();
|
|
1215
|
+
const items = Array.isArray(data) ? data : data?.clients ?? data?.data ?? [];
|
|
1216
|
+
return { applications: items };
|
|
1217
|
+
},
|
|
1218
|
+
/**
|
|
1219
|
+
* Delete an OAuth application by its `client_id`.
|
|
1220
|
+
* POST /api/v1/auth/oauth2/delete-client
|
|
1221
|
+
*
|
|
1222
|
+
* Tokens and consents referencing the client cascade-delete via the
|
|
1223
|
+
* better-auth schema's `onDelete: cascade` foreign keys.
|
|
1224
|
+
*/
|
|
1225
|
+
delete: async (clientId) => {
|
|
1226
|
+
const route = this.getRoute("auth");
|
|
1227
|
+
const res = await this.fetch(`${this.baseUrl}${route}/oauth2/delete-client`, {
|
|
1228
|
+
method: "POST",
|
|
1229
|
+
body: JSON.stringify({ client_id: clientId })
|
|
1230
|
+
});
|
|
1231
|
+
return res.json();
|
|
1232
|
+
}
|
|
1233
|
+
},
|
|
1234
|
+
/**
|
|
1235
|
+
* Submit the user's decision to a pending consent request.
|
|
1236
|
+
* POST /api/v1/auth/oauth2/consent
|
|
1237
|
+
*
|
|
1238
|
+
* Called by the consent screen after the user accepts or denies. The
|
|
1239
|
+
* `oauth_query` is the raw query string of the consent page URL — it
|
|
1240
|
+
* carries the signed authorization request that the consent endpoint
|
|
1241
|
+
* verifies before issuing the authorization code.
|
|
1242
|
+
*/
|
|
1243
|
+
consent: async (req) => {
|
|
1244
|
+
const route = this.getRoute("auth");
|
|
1245
|
+
const res = await this.fetch(`${this.baseUrl}${route}/oauth2/consent`, {
|
|
1246
|
+
method: "POST",
|
|
1247
|
+
body: JSON.stringify(req)
|
|
1248
|
+
});
|
|
1249
|
+
return res.json();
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
634
1252
|
/**
|
|
635
1253
|
* Authentication Services
|
|
636
1254
|
*/
|
|
637
1255
|
this.auth = {
|
|
1256
|
+
/**
|
|
1257
|
+
* Get authentication configuration
|
|
1258
|
+
* Returns available auth providers and features
|
|
1259
|
+
*/
|
|
1260
|
+
getConfig: async () => {
|
|
1261
|
+
const route = this.getRoute("auth");
|
|
1262
|
+
const res = await this.fetch(`${this.baseUrl}${route}/config`);
|
|
1263
|
+
return this.unwrapResponse(res);
|
|
1264
|
+
},
|
|
638
1265
|
/**
|
|
639
1266
|
* Login with email and password
|
|
640
1267
|
* Uses better-auth endpoint: POST /sign-in/email
|
|
@@ -643,8 +1270,100 @@ var ObjectStackClient = class {
|
|
|
643
1270
|
const route = this.getRoute("auth");
|
|
644
1271
|
const res = await this.fetch(`${this.baseUrl}${route}/sign-in/email`, {
|
|
645
1272
|
method: "POST",
|
|
1273
|
+
headers: { Origin: this.baseUrl },
|
|
646
1274
|
body: JSON.stringify(request)
|
|
647
1275
|
});
|
|
1276
|
+
const raw = await res.json();
|
|
1277
|
+
const data = raw && (raw.data ?? (raw.token || raw.user ? { token: raw.token, user: raw.user } : void 0));
|
|
1278
|
+
const normalized = data ? { ...raw, data } : raw;
|
|
1279
|
+
if (normalized.data?.token) {
|
|
1280
|
+
this.token = normalized.data.token;
|
|
1281
|
+
}
|
|
1282
|
+
return normalized;
|
|
1283
|
+
},
|
|
1284
|
+
/**
|
|
1285
|
+
* Logout current user
|
|
1286
|
+
* Uses better-auth endpoint: POST /sign-out
|
|
1287
|
+
*/
|
|
1288
|
+
logout: async () => {
|
|
1289
|
+
const route = this.getRoute("auth");
|
|
1290
|
+
await this.fetch(`${this.baseUrl}${route}/sign-out`, {
|
|
1291
|
+
method: "POST",
|
|
1292
|
+
headers: { "Content-Type": "application/json", Origin: this.baseUrl },
|
|
1293
|
+
body: "{}"
|
|
1294
|
+
});
|
|
1295
|
+
this.token = void 0;
|
|
1296
|
+
},
|
|
1297
|
+
/**
|
|
1298
|
+
* Get current user session
|
|
1299
|
+
* Uses better-auth endpoint: GET /get-session
|
|
1300
|
+
*/
|
|
1301
|
+
me: async () => {
|
|
1302
|
+
const route = this.getRoute("auth");
|
|
1303
|
+
const res = await this.fetch(`${this.baseUrl}${route}/get-session`, {
|
|
1304
|
+
headers: { Origin: this.baseUrl }
|
|
1305
|
+
});
|
|
1306
|
+
return res.json();
|
|
1307
|
+
},
|
|
1308
|
+
/**
|
|
1309
|
+
* Register a new user account
|
|
1310
|
+
* Uses better-auth endpoint: POST /sign-up/email
|
|
1311
|
+
*/
|
|
1312
|
+
register: async (request) => {
|
|
1313
|
+
const route = this.getRoute("auth");
|
|
1314
|
+
const res = await this.fetch(`${this.baseUrl}${route}/sign-up/email`, {
|
|
1315
|
+
method: "POST",
|
|
1316
|
+
headers: { Origin: this.baseUrl },
|
|
1317
|
+
body: JSON.stringify(request)
|
|
1318
|
+
});
|
|
1319
|
+
const raw = await res.json();
|
|
1320
|
+
const data = raw && (raw.data ?? (raw.token || raw.user ? { token: raw.token, user: raw.user } : void 0));
|
|
1321
|
+
const normalized = data ? { ...raw, data } : raw;
|
|
1322
|
+
if (normalized.data?.token) {
|
|
1323
|
+
this.token = normalized.data.token;
|
|
1324
|
+
}
|
|
1325
|
+
return normalized;
|
|
1326
|
+
},
|
|
1327
|
+
/**
|
|
1328
|
+
* Initiate OAuth sign-in via a social or OIDC provider.
|
|
1329
|
+
*
|
|
1330
|
+
* - Social providers (Google, GitHub, etc.): calls POST /sign-in/social with `{ provider }`.
|
|
1331
|
+
* - OIDC/enterprise providers: calls POST /sign-in/oauth2 with `{ providerId }`.
|
|
1332
|
+
*
|
|
1333
|
+
* After the provider callback better-auth sets the session cookie and redirects to `callbackURL`.
|
|
1334
|
+
*/
|
|
1335
|
+
signInWithProvider: async (provider, opts) => {
|
|
1336
|
+
if (typeof window === "undefined") {
|
|
1337
|
+
throw new Error("signInWithProvider requires a browser environment");
|
|
1338
|
+
}
|
|
1339
|
+
const route = this.getRoute("auth");
|
|
1340
|
+
const callbackURL = opts?.callbackURL ?? window.location.origin + "/login";
|
|
1341
|
+
const isOidc = opts?.type === "oidc";
|
|
1342
|
+
const endpoint = isOidc ? "/sign-in/oauth2" : "/sign-in/social";
|
|
1343
|
+
const body = isOidc ? { providerId: provider, callbackURL } : { provider, callbackURL };
|
|
1344
|
+
if (opts?.errorCallbackURL) body.errorCallbackURL = opts.errorCallbackURL;
|
|
1345
|
+
const res = await this.fetch(`${this.baseUrl}${route}${endpoint}`, {
|
|
1346
|
+
method: "POST",
|
|
1347
|
+
body: JSON.stringify(body)
|
|
1348
|
+
});
|
|
1349
|
+
const data = await res.json();
|
|
1350
|
+
const redirectUrl = data?.url ?? data?.data?.url;
|
|
1351
|
+
if (redirectUrl) {
|
|
1352
|
+
window.location.assign(redirectUrl);
|
|
1353
|
+
} else {
|
|
1354
|
+
throw new Error(`signInWithProvider: no redirect URL returned for provider "${provider}"`);
|
|
1355
|
+
}
|
|
1356
|
+
},
|
|
1357
|
+
/**
|
|
1358
|
+
* Refresh an authentication token
|
|
1359
|
+
* Note: better-auth handles token refresh automatically via /get-session
|
|
1360
|
+
* @param _refreshToken - Not used (better-auth handles refresh automatically)
|
|
1361
|
+
*/
|
|
1362
|
+
refreshToken: async (_refreshToken) => {
|
|
1363
|
+
const route = this.getRoute("auth");
|
|
1364
|
+
const res = await this.fetch(`${this.baseUrl}${route}/get-session`, {
|
|
1365
|
+
method: "GET"
|
|
1366
|
+
});
|
|
648
1367
|
const data = await res.json();
|
|
649
1368
|
if (data.data?.token) {
|
|
650
1369
|
this.token = data.data.token;
|
|
@@ -652,54 +1371,262 @@ var ObjectStackClient = class {
|
|
|
652
1371
|
return data;
|
|
653
1372
|
},
|
|
654
1373
|
/**
|
|
655
|
-
*
|
|
656
|
-
*
|
|
1374
|
+
* Probe the framework-only `/auth/bootstrap-status` endpoint to determine
|
|
1375
|
+
* whether the very first owner has been provisioned. The Account portal's
|
|
1376
|
+
* `/setup` route uses this to decide whether to render the bootstrap form
|
|
1377
|
+
* or bounce the user straight to `/login`.
|
|
1378
|
+
*/
|
|
1379
|
+
bootstrapStatus: async () => {
|
|
1380
|
+
const route = this.getRoute("auth");
|
|
1381
|
+
const res = await this.fetch(`${this.baseUrl}${route}/bootstrap-status`);
|
|
1382
|
+
const data = await res.json();
|
|
1383
|
+
const payload = data?.data ?? data;
|
|
1384
|
+
return { hasOwner: !!payload?.hasOwner };
|
|
1385
|
+
},
|
|
1386
|
+
/**
|
|
1387
|
+
* Update the current user's profile.
|
|
1388
|
+
*
|
|
1389
|
+
* better-auth: POST /update-user — accepts `{ name?, image?, ... }`
|
|
1390
|
+
* (any custom user fields configured on the server). Returns the
|
|
1391
|
+
* updated user.
|
|
1392
|
+
*/
|
|
1393
|
+
updateUser: async (data) => {
|
|
1394
|
+
const route = this.getRoute("auth");
|
|
1395
|
+
const res = await this.fetch(`${this.baseUrl}${route}/update-user`, {
|
|
1396
|
+
method: "POST",
|
|
1397
|
+
body: JSON.stringify(data)
|
|
1398
|
+
});
|
|
1399
|
+
return res.json();
|
|
1400
|
+
},
|
|
1401
|
+
/**
|
|
1402
|
+
* Change the current user's password (email/password accounts only).
|
|
1403
|
+
*
|
|
1404
|
+
* better-auth: POST /change-password.
|
|
1405
|
+
* Set `revokeOtherSessions: true` to invalidate every other session
|
|
1406
|
+
* after the change.
|
|
1407
|
+
*/
|
|
1408
|
+
changePassword: async (req) => {
|
|
1409
|
+
const route = this.getRoute("auth");
|
|
1410
|
+
const res = await this.fetch(`${this.baseUrl}${route}/change-password`, {
|
|
1411
|
+
method: "POST",
|
|
1412
|
+
body: JSON.stringify(req)
|
|
1413
|
+
});
|
|
1414
|
+
return res.json();
|
|
1415
|
+
},
|
|
1416
|
+
/**
|
|
1417
|
+
* Begin a change-email flow. better-auth sends a verification mail to
|
|
1418
|
+
* the new address; the change only takes effect after the user clicks
|
|
1419
|
+
* the link.
|
|
1420
|
+
*
|
|
1421
|
+
* better-auth: POST /change-email — `{ newEmail, callbackURL? }`.
|
|
1422
|
+
*/
|
|
1423
|
+
changeEmail: async (req) => {
|
|
1424
|
+
const route = this.getRoute("auth");
|
|
1425
|
+
const res = await this.fetch(`${this.baseUrl}${route}/change-email`, {
|
|
1426
|
+
method: "POST",
|
|
1427
|
+
body: JSON.stringify(req)
|
|
1428
|
+
});
|
|
1429
|
+
return res.json();
|
|
1430
|
+
},
|
|
1431
|
+
/**
|
|
1432
|
+
* Re-send the email-verification link to the current user (or any
|
|
1433
|
+
* address when called as an admin). better-auth: POST /send-verification-email.
|
|
657
1434
|
*/
|
|
658
|
-
|
|
1435
|
+
sendVerificationEmail: async (req) => {
|
|
659
1436
|
const route = this.getRoute("auth");
|
|
660
|
-
await this.fetch(`${this.baseUrl}${route}/
|
|
661
|
-
|
|
1437
|
+
const res = await this.fetch(`${this.baseUrl}${route}/send-verification-email`, {
|
|
1438
|
+
method: "POST",
|
|
1439
|
+
body: JSON.stringify(req)
|
|
1440
|
+
});
|
|
1441
|
+
return res.json();
|
|
662
1442
|
},
|
|
663
1443
|
/**
|
|
664
|
-
*
|
|
665
|
-
*
|
|
1444
|
+
* Verify an email-verification token (the link target).
|
|
1445
|
+
*
|
|
1446
|
+
* better-auth: GET /verify-email?token=…&callbackURL=…
|
|
666
1447
|
*/
|
|
667
|
-
|
|
1448
|
+
verifyEmail: async (params) => {
|
|
668
1449
|
const route = this.getRoute("auth");
|
|
669
|
-
const
|
|
1450
|
+
const url = new URL(`${this.baseUrl}${route}/verify-email`);
|
|
1451
|
+
url.searchParams.set("token", params.token);
|
|
1452
|
+
if (params.callbackURL) url.searchParams.set("callbackURL", params.callbackURL);
|
|
1453
|
+
const res = await this.fetch(url.toString());
|
|
670
1454
|
return res.json();
|
|
671
1455
|
},
|
|
672
1456
|
/**
|
|
673
|
-
*
|
|
674
|
-
*
|
|
1457
|
+
* Permanently delete the current user. better-auth supports two flows:
|
|
1458
|
+
*
|
|
1459
|
+
* 1. With a fresh-session password challenge: POST `{ password }`.
|
|
1460
|
+
* 2. With an emailed deletion-confirmation token: POST `{ token }`,
|
|
1461
|
+
* typically following an out-of-band confirmation step.
|
|
1462
|
+
*
|
|
1463
|
+
* Server policy decides which is required; pass whichever you have.
|
|
675
1464
|
*/
|
|
676
|
-
|
|
1465
|
+
deleteUser: async (req) => {
|
|
677
1466
|
const route = this.getRoute("auth");
|
|
678
|
-
const res = await this.fetch(`${this.baseUrl}${route}/
|
|
1467
|
+
const res = await this.fetch(`${this.baseUrl}${route}/delete-user`, {
|
|
679
1468
|
method: "POST",
|
|
680
|
-
body: JSON.stringify(
|
|
1469
|
+
body: JSON.stringify(req)
|
|
681
1470
|
});
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
1471
|
+
this.token = void 0;
|
|
1472
|
+
return res.json();
|
|
1473
|
+
},
|
|
1474
|
+
/**
|
|
1475
|
+
* Active-session management. Wraps better-auth's session endpoints so
|
|
1476
|
+
* the Account portal's `/account/sessions` page can list every device
|
|
1477
|
+
* the user is signed in from and revoke them individually or in bulk.
|
|
1478
|
+
*/
|
|
1479
|
+
sessions: {
|
|
1480
|
+
/** better-auth: GET /list-sessions — returns the current user's sessions. */
|
|
1481
|
+
list: async () => {
|
|
1482
|
+
const route = this.getRoute("auth");
|
|
1483
|
+
const res = await this.fetch(`${this.baseUrl}${route}/list-sessions`);
|
|
1484
|
+
const data = await res.json();
|
|
1485
|
+
const sessions = Array.isArray(data) ? data : data?.data ?? data?.sessions ?? [];
|
|
1486
|
+
return { sessions };
|
|
1487
|
+
},
|
|
1488
|
+
/** better-auth: POST /revoke-session — revoke a single session by token. */
|
|
1489
|
+
revoke: async (token) => {
|
|
1490
|
+
const route = this.getRoute("auth");
|
|
1491
|
+
const res = await this.fetch(`${this.baseUrl}${route}/revoke-session`, {
|
|
1492
|
+
method: "POST",
|
|
1493
|
+
body: JSON.stringify({ token })
|
|
1494
|
+
});
|
|
1495
|
+
return res.json();
|
|
1496
|
+
},
|
|
1497
|
+
/** better-auth: POST /revoke-other-sessions — keep current, kill the rest. */
|
|
1498
|
+
revokeOthers: async () => {
|
|
1499
|
+
const route = this.getRoute("auth");
|
|
1500
|
+
const res = await this.fetch(`${this.baseUrl}${route}/revoke-other-sessions`, {
|
|
1501
|
+
method: "POST",
|
|
1502
|
+
body: "{}"
|
|
1503
|
+
});
|
|
1504
|
+
return res.json();
|
|
1505
|
+
},
|
|
1506
|
+
/** better-auth: POST /revoke-sessions — kill every session for this user. */
|
|
1507
|
+
revokeAll: async () => {
|
|
1508
|
+
const route = this.getRoute("auth");
|
|
1509
|
+
const res = await this.fetch(`${this.baseUrl}${route}/revoke-sessions`, {
|
|
1510
|
+
method: "POST",
|
|
1511
|
+
body: "{}"
|
|
1512
|
+
});
|
|
1513
|
+
this.token = void 0;
|
|
1514
|
+
return res.json();
|
|
685
1515
|
}
|
|
686
|
-
return data;
|
|
687
1516
|
},
|
|
688
1517
|
/**
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
1518
|
+
* Two-factor authentication (TOTP + backup codes). Requires the
|
|
1519
|
+
* `twoFactor` plugin to be enabled on the server (see
|
|
1520
|
+
* `plugin-auth` config). Endpoints live under `/two-factor/*`.
|
|
692
1521
|
*/
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
1522
|
+
twoFactor: {
|
|
1523
|
+
/**
|
|
1524
|
+
* Start enrolment. Server returns a TOTP URI (`otpauth://...`) which
|
|
1525
|
+
* the UI renders as a QR code; the user then calls `verifyTotp` to
|
|
1526
|
+
* confirm and finish enabling.
|
|
1527
|
+
*/
|
|
1528
|
+
enable: async (req) => {
|
|
1529
|
+
const route = this.getRoute("auth");
|
|
1530
|
+
const res = await this.fetch(`${this.baseUrl}${route}/two-factor/enable`, {
|
|
1531
|
+
method: "POST",
|
|
1532
|
+
body: JSON.stringify(req)
|
|
1533
|
+
});
|
|
1534
|
+
const data = await res.json();
|
|
1535
|
+
return data?.data ?? data;
|
|
1536
|
+
},
|
|
1537
|
+
/**
|
|
1538
|
+
* Confirm a TOTP code — used to finalise enrolment after `enable()`
|
|
1539
|
+
* or to step up an existing 2FA-enabled session. `trustDevice` (when
|
|
1540
|
+
* supported by the server config) suppresses the 2FA challenge on
|
|
1541
|
+
* this browser for the configured trust period.
|
|
1542
|
+
*/
|
|
1543
|
+
verifyTotp: async (req) => {
|
|
1544
|
+
const route = this.getRoute("auth");
|
|
1545
|
+
const res = await this.fetch(`${this.baseUrl}${route}/two-factor/verify-totp`, {
|
|
1546
|
+
method: "POST",
|
|
1547
|
+
body: JSON.stringify(req)
|
|
1548
|
+
});
|
|
1549
|
+
return res.json();
|
|
1550
|
+
},
|
|
1551
|
+
/** Disable 2FA for the current user. Requires the password again. */
|
|
1552
|
+
disable: async (req) => {
|
|
1553
|
+
const route = this.getRoute("auth");
|
|
1554
|
+
const res = await this.fetch(`${this.baseUrl}${route}/two-factor/disable`, {
|
|
1555
|
+
method: "POST",
|
|
1556
|
+
body: JSON.stringify(req)
|
|
1557
|
+
});
|
|
1558
|
+
return res.json();
|
|
1559
|
+
},
|
|
1560
|
+
/**
|
|
1561
|
+
* Issue a fresh set of backup codes (invalidating any previous set).
|
|
1562
|
+
* Display them once — the server only stores hashes.
|
|
1563
|
+
*/
|
|
1564
|
+
generateBackupCodes: async (req) => {
|
|
1565
|
+
const route = this.getRoute("auth");
|
|
1566
|
+
const res = await this.fetch(`${this.baseUrl}${route}/two-factor/generate-backup-codes`, {
|
|
1567
|
+
method: "POST",
|
|
1568
|
+
body: JSON.stringify(req)
|
|
1569
|
+
});
|
|
1570
|
+
const data = await res.json();
|
|
1571
|
+
return data?.data ?? data;
|
|
1572
|
+
},
|
|
1573
|
+
/**
|
|
1574
|
+
* Verify a 2FA backup code in lieu of a TOTP. Useful as a recovery
|
|
1575
|
+
* affordance when the user has lost their authenticator app.
|
|
1576
|
+
*/
|
|
1577
|
+
verifyBackupCode: async (req) => {
|
|
1578
|
+
const route = this.getRoute("auth");
|
|
1579
|
+
const res = await this.fetch(`${this.baseUrl}${route}/two-factor/verify-backup-code`, {
|
|
1580
|
+
method: "POST",
|
|
1581
|
+
body: JSON.stringify(req)
|
|
1582
|
+
});
|
|
1583
|
+
return res.json();
|
|
1584
|
+
}
|
|
1585
|
+
},
|
|
1586
|
+
/**
|
|
1587
|
+
* Linked credentials — i.e. the rows in better-auth's `account` table
|
|
1588
|
+
* (one per provider × user). Lets the user see and unlink their social
|
|
1589
|
+
* / OIDC connections from the Account portal.
|
|
1590
|
+
*/
|
|
1591
|
+
accounts: {
|
|
1592
|
+
/** better-auth: GET /list-accounts */
|
|
1593
|
+
list: async () => {
|
|
1594
|
+
const route = this.getRoute("auth");
|
|
1595
|
+
const res = await this.fetch(`${this.baseUrl}${route}/list-accounts`);
|
|
1596
|
+
const data = await res.json();
|
|
1597
|
+
const accounts = Array.isArray(data) ? data : data?.data ?? data?.accounts ?? [];
|
|
1598
|
+
return { accounts };
|
|
1599
|
+
},
|
|
1600
|
+
/**
|
|
1601
|
+
* Unlink a provider connection.
|
|
1602
|
+
* better-auth: POST /unlink-account — `{ providerId, accountId? }`.
|
|
1603
|
+
* `accountId` is required when the user has more than one account
|
|
1604
|
+
* for the same provider.
|
|
1605
|
+
*/
|
|
1606
|
+
unlink: async (req) => {
|
|
1607
|
+
const route = this.getRoute("auth");
|
|
1608
|
+
const res = await this.fetch(`${this.baseUrl}${route}/unlink-account`, {
|
|
1609
|
+
method: "POST",
|
|
1610
|
+
body: JSON.stringify(req)
|
|
1611
|
+
});
|
|
1612
|
+
return res.json();
|
|
1613
|
+
},
|
|
1614
|
+
/**
|
|
1615
|
+
* Link an additional social provider to the current user.
|
|
1616
|
+
* better-auth: POST /link-social — `{ provider, callbackURL }`. The
|
|
1617
|
+
* server returns a redirect URL; the caller should `window.location`
|
|
1618
|
+
* to it (mirroring `signInWithProvider`).
|
|
1619
|
+
*/
|
|
1620
|
+
linkSocial: async (req) => {
|
|
1621
|
+
const route = this.getRoute("auth");
|
|
1622
|
+
const callbackURL = req.callbackURL ?? (typeof window !== "undefined" ? window.location.href : void 0);
|
|
1623
|
+
const res = await this.fetch(`${this.baseUrl}${route}/link-social`, {
|
|
1624
|
+
method: "POST",
|
|
1625
|
+
body: JSON.stringify({ provider: req.provider, callbackURL })
|
|
1626
|
+
});
|
|
1627
|
+
const data = await res.json();
|
|
1628
|
+
return data?.data ?? data;
|
|
701
1629
|
}
|
|
702
|
-
return data;
|
|
703
1630
|
}
|
|
704
1631
|
};
|
|
705
1632
|
/**
|
|
@@ -906,6 +1833,46 @@ var ObjectStackClient = class {
|
|
|
906
1833
|
const res = await this.fetch(`${this.baseUrl}${route}/${flowName}/runs/${runId}`);
|
|
907
1834
|
return this.unwrapResponse(res);
|
|
908
1835
|
}
|
|
1836
|
+
},
|
|
1837
|
+
/**
|
|
1838
|
+
* Flat aliases mirroring the ScopedProjectClient.automation surface so
|
|
1839
|
+
* Studio (and other consumers) can use the same call shape regardless of
|
|
1840
|
+
* whether they hold a scoped or unscoped client.
|
|
1841
|
+
*/
|
|
1842
|
+
/** Alias for `automation.get` — fetch a flow definition by name. */
|
|
1843
|
+
getFlow: async (name) => {
|
|
1844
|
+
const route = this.getRoute("automation");
|
|
1845
|
+
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(name)}`);
|
|
1846
|
+
return this.unwrapResponse(res);
|
|
1847
|
+
},
|
|
1848
|
+
/** Execute (trigger) a flow with an execution context. */
|
|
1849
|
+
execute: async (name, ctx) => {
|
|
1850
|
+
const route = this.getRoute("automation");
|
|
1851
|
+
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(name)}/trigger`, {
|
|
1852
|
+
method: "POST",
|
|
1853
|
+
body: JSON.stringify(ctx ?? {})
|
|
1854
|
+
});
|
|
1855
|
+
return this.unwrapResponse(res);
|
|
1856
|
+
},
|
|
1857
|
+
/** Alias for `automation.runs.list`. */
|
|
1858
|
+
listRuns: async (flowName, opts) => {
|
|
1859
|
+
const route = this.getRoute("automation");
|
|
1860
|
+
const params = new URLSearchParams();
|
|
1861
|
+
if (opts?.limit != null) params.set("limit", String(opts.limit));
|
|
1862
|
+
if (opts?.cursor) params.set("cursor", opts.cursor);
|
|
1863
|
+
const qs = params.toString();
|
|
1864
|
+
const res = await this.fetch(
|
|
1865
|
+
`${this.baseUrl}${route}/${encodeURIComponent(flowName)}/runs${qs ? `?${qs}` : ""}`
|
|
1866
|
+
);
|
|
1867
|
+
return this.unwrapResponse(res);
|
|
1868
|
+
},
|
|
1869
|
+
/** Alias for `automation.runs.get`. */
|
|
1870
|
+
getRun: async (flowName, runId) => {
|
|
1871
|
+
const route = this.getRoute("automation");
|
|
1872
|
+
const res = await this.fetch(
|
|
1873
|
+
`${this.baseUrl}${route}/${encodeURIComponent(flowName)}/runs/${encodeURIComponent(runId)}`
|
|
1874
|
+
);
|
|
1875
|
+
return this.unwrapResponse(res);
|
|
909
1876
|
}
|
|
910
1877
|
};
|
|
911
1878
|
/**
|
|
@@ -1596,6 +2563,7 @@ var ObjectStackClient = class {
|
|
|
1596
2563
|
};
|
|
1597
2564
|
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
1598
2565
|
this.token = config.token;
|
|
2566
|
+
this.projectId = config.projectId;
|
|
1599
2567
|
this.fetchImpl = config.fetch || globalThis.fetch.bind(globalThis);
|
|
1600
2568
|
this.logger = config.logger || createLogger({
|
|
1601
2569
|
level: config.debug ? "debug" : "info",
|
|
@@ -1612,6 +2580,18 @@ var ObjectStackClient = class {
|
|
|
1612
2580
|
try {
|
|
1613
2581
|
let data;
|
|
1614
2582
|
try {
|
|
2583
|
+
const discoveryUrl = `${this.baseUrl}/api/v1/discovery`;
|
|
2584
|
+
this.logger.debug("Probing protocol-standard discovery endpoint", { url: discoveryUrl });
|
|
2585
|
+
const res = await this.fetchImpl(discoveryUrl);
|
|
2586
|
+
if (res.ok) {
|
|
2587
|
+
const body = await res.json();
|
|
2588
|
+
data = body.data || body;
|
|
2589
|
+
this.logger.debug("Discovered via /api/v1/discovery");
|
|
2590
|
+
}
|
|
2591
|
+
} catch (e) {
|
|
2592
|
+
this.logger.debug("Protocol-standard discovery probe failed", { error: e.message });
|
|
2593
|
+
}
|
|
2594
|
+
if (!data) {
|
|
1615
2595
|
let wellKnownUrl;
|
|
1616
2596
|
try {
|
|
1617
2597
|
const url = new URL(this.baseUrl);
|
|
@@ -1619,22 +2599,10 @@ var ObjectStackClient = class {
|
|
|
1619
2599
|
} catch {
|
|
1620
2600
|
wellKnownUrl = "/.well-known/objectstack";
|
|
1621
2601
|
}
|
|
1622
|
-
this.logger.debug("
|
|
2602
|
+
this.logger.debug("Falling back to .well-known discovery", { url: wellKnownUrl });
|
|
1623
2603
|
const res = await this.fetchImpl(wellKnownUrl);
|
|
1624
|
-
if (res.ok) {
|
|
1625
|
-
const body = await res.json();
|
|
1626
|
-
data = body.data || body;
|
|
1627
|
-
this.logger.debug("Discovered via .well-known");
|
|
1628
|
-
}
|
|
1629
|
-
} catch (e) {
|
|
1630
|
-
this.logger.debug("Standard discovery probe failed", { error: e.message });
|
|
1631
|
-
}
|
|
1632
|
-
if (!data) {
|
|
1633
|
-
const fallbackUrl = `${this.baseUrl}/api/v1/discovery`;
|
|
1634
|
-
this.logger.debug("Falling back to standard discovery endpoint", { url: fallbackUrl });
|
|
1635
|
-
const res = await this.fetchImpl(fallbackUrl);
|
|
1636
2604
|
if (!res.ok) {
|
|
1637
|
-
throw new Error(`Failed to connect to ${
|
|
2605
|
+
throw new Error(`Failed to connect to ${wellKnownUrl}: ${res.statusText}`);
|
|
1638
2606
|
}
|
|
1639
2607
|
const body = await res.json();
|
|
1640
2608
|
data = body.data || body;
|
|
@@ -1672,6 +2640,64 @@ var ObjectStackClient = class {
|
|
|
1672
2640
|
}
|
|
1673
2641
|
return result;
|
|
1674
2642
|
}
|
|
2643
|
+
/**
|
|
2644
|
+
* Project-scoped client factory.
|
|
2645
|
+
*
|
|
2646
|
+
* Returns a thin wrapper around the data / meta / packages namespaces that
|
|
2647
|
+
* prefixes every request with `/api/v1/projects/:projectId/...`. Use this
|
|
2648
|
+
* when the server has `enableProjectScoping: true` in its REST API config.
|
|
2649
|
+
*
|
|
2650
|
+
* Backward compatibility: `client.data.*`, `client.meta.*`, and
|
|
2651
|
+
* `client.packages.*` continue to work unchanged; they hit unscoped routes
|
|
2652
|
+
* and rely on hostname / `X-Project-Id` header / session resolution.
|
|
2653
|
+
*
|
|
2654
|
+
* @example
|
|
2655
|
+
* ```ts
|
|
2656
|
+
* const scoped = client.project('00000000-0000-0000-0000-000000000001');
|
|
2657
|
+
* const tasks = await scoped.data.find('task', { top: 10 });
|
|
2658
|
+
* const objects = await scoped.meta.getItems('object');
|
|
2659
|
+
* ```
|
|
2660
|
+
*/
|
|
2661
|
+
project(projectId) {
|
|
2662
|
+
if (!projectId) {
|
|
2663
|
+
throw new Error("[ObjectStack] project(id): projectId is required");
|
|
2664
|
+
}
|
|
2665
|
+
return new ScopedProjectClient(this, projectId);
|
|
2666
|
+
}
|
|
2667
|
+
// ── Internal accessors exposed to ScopedProjectClient ────────────────
|
|
2668
|
+
// The scoped client lives in the same module so using module-level access
|
|
2669
|
+
// works; TypeScript requires these to be accessible, so we expose them via
|
|
2670
|
+
// small protected getters that keep the public surface unchanged.
|
|
2671
|
+
/** @internal */
|
|
2672
|
+
_baseUrl() {
|
|
2673
|
+
return this.baseUrl;
|
|
2674
|
+
}
|
|
2675
|
+
/** @internal */
|
|
2676
|
+
_fetch(url, init) {
|
|
2677
|
+
return this.fetch(url, init);
|
|
2678
|
+
}
|
|
2679
|
+
/** @internal */
|
|
2680
|
+
_unwrap(res) {
|
|
2681
|
+
return this.unwrapResponse(res);
|
|
2682
|
+
}
|
|
2683
|
+
/** @internal */
|
|
2684
|
+
_isFilterAST(v) {
|
|
2685
|
+
return this.isFilterAST(v);
|
|
2686
|
+
}
|
|
2687
|
+
/**
|
|
2688
|
+
* Update the active project id used for subsequent requests.
|
|
2689
|
+
* Pass `undefined` to clear (falls back to the session default).
|
|
2690
|
+
*/
|
|
2691
|
+
setProjectId(projectId) {
|
|
2692
|
+
this.projectId = projectId;
|
|
2693
|
+
this.logger.debug("Active project changed", { projectId });
|
|
2694
|
+
}
|
|
2695
|
+
/**
|
|
2696
|
+
* Current active project id (if set).
|
|
2697
|
+
*/
|
|
2698
|
+
getProjectId() {
|
|
2699
|
+
return this.projectId;
|
|
2700
|
+
}
|
|
1675
2701
|
/**
|
|
1676
2702
|
* Event Subscription API
|
|
1677
2703
|
* Provides real-time event subscriptions for metadata and data changes
|
|
@@ -1712,6 +2738,9 @@ var ObjectStackClient = class {
|
|
|
1712
2738
|
if (this.token) {
|
|
1713
2739
|
headers["Authorization"] = `Bearer ${this.token}`;
|
|
1714
2740
|
}
|
|
2741
|
+
if (this.projectId) {
|
|
2742
|
+
headers["X-Project-Id"] = this.projectId;
|
|
2743
|
+
}
|
|
1715
2744
|
const res = await this.fetchImpl(url, { ...options, headers });
|
|
1716
2745
|
this.logger.debug("HTTP response", {
|
|
1717
2746
|
method: options.method || "GET",
|
|
@@ -1778,11 +2807,246 @@ var ObjectStackClient = class {
|
|
|
1778
2807
|
return routeMap[type] || `/api/v1/${type}`;
|
|
1779
2808
|
}
|
|
1780
2809
|
};
|
|
2810
|
+
var ScopedProjectClient = class {
|
|
2811
|
+
constructor(parent, projectId) {
|
|
2812
|
+
/**
|
|
2813
|
+
* Metadata operations scoped to this project.
|
|
2814
|
+
*/
|
|
2815
|
+
this.meta = {
|
|
2816
|
+
getTypes: async () => {
|
|
2817
|
+
const res = await this.parent._fetch(this.url("/meta"));
|
|
2818
|
+
return this.parent._unwrap(res);
|
|
2819
|
+
},
|
|
2820
|
+
getItems: async (type, options) => {
|
|
2821
|
+
const params = new URLSearchParams();
|
|
2822
|
+
if (options?.packageId) params.set("package", options.packageId);
|
|
2823
|
+
const qs = params.toString();
|
|
2824
|
+
const res = await this.parent._fetch(this.url(`/meta/${type}${qs ? `?${qs}` : ""}`));
|
|
2825
|
+
return this.parent._unwrap(res);
|
|
2826
|
+
},
|
|
2827
|
+
getItem: async (type, name, options) => {
|
|
2828
|
+
const params = new URLSearchParams();
|
|
2829
|
+
if (options?.packageId) params.set("package", options.packageId);
|
|
2830
|
+
const qs = params.toString();
|
|
2831
|
+
const res = await this.parent._fetch(this.url(`/meta/${type}/${name}${qs ? `?${qs}` : ""}`));
|
|
2832
|
+
return this.parent._unwrap(res);
|
|
2833
|
+
},
|
|
2834
|
+
saveItem: async (type, name, item) => {
|
|
2835
|
+
const res = await this.parent._fetch(this.url(`/meta/${type}/${name}`), {
|
|
2836
|
+
method: "PUT",
|
|
2837
|
+
body: JSON.stringify(item)
|
|
2838
|
+
});
|
|
2839
|
+
return this.parent._unwrap(res);
|
|
2840
|
+
},
|
|
2841
|
+
deleteItem: async (type, name) => {
|
|
2842
|
+
const res = await this.parent._fetch(this.url(`/meta/${encodeURIComponent(type)}/${encodeURIComponent(name)}`), {
|
|
2843
|
+
method: "DELETE"
|
|
2844
|
+
});
|
|
2845
|
+
return this.parent._unwrap(res);
|
|
2846
|
+
}
|
|
2847
|
+
};
|
|
2848
|
+
/**
|
|
2849
|
+
* Data operations scoped to this project.
|
|
2850
|
+
*
|
|
2851
|
+
* Mirrors the query / find / get / create / update / delete / batch
|
|
2852
|
+
* surface on {@link ObjectStackClient}. URL construction differs only
|
|
2853
|
+
* in the prefix — query parameter serialization is identical.
|
|
2854
|
+
*/
|
|
2855
|
+
this.data = {
|
|
2856
|
+
query: async (object, query) => {
|
|
2857
|
+
const res = await this.parent._fetch(this.url(`/data/${object}/query`), {
|
|
2858
|
+
method: "POST",
|
|
2859
|
+
body: JSON.stringify(query)
|
|
2860
|
+
});
|
|
2861
|
+
return this.parent._unwrap(res);
|
|
2862
|
+
},
|
|
2863
|
+
find: async (object, options = {}) => {
|
|
2864
|
+
const queryParams = new URLSearchParams();
|
|
2865
|
+
const v2 = options;
|
|
2866
|
+
const normalizedOptions = {};
|
|
2867
|
+
if ("where" in options || "fields" in options || "orderBy" in options || "offset" in options) {
|
|
2868
|
+
if (v2.where) normalizedOptions.filter = v2.where;
|
|
2869
|
+
if (v2.fields) normalizedOptions.select = v2.fields;
|
|
2870
|
+
if (v2.orderBy) normalizedOptions.sort = v2.orderBy;
|
|
2871
|
+
if (v2.limit != null) normalizedOptions.top = v2.limit;
|
|
2872
|
+
if (v2.offset != null) normalizedOptions.skip = v2.offset;
|
|
2873
|
+
if (v2.aggregations) normalizedOptions.aggregations = v2.aggregations;
|
|
2874
|
+
if (v2.groupBy) normalizedOptions.groupBy = v2.groupBy;
|
|
2875
|
+
} else {
|
|
2876
|
+
Object.assign(normalizedOptions, options);
|
|
2877
|
+
}
|
|
2878
|
+
if (normalizedOptions.top) queryParams.set("top", normalizedOptions.top.toString());
|
|
2879
|
+
if (normalizedOptions.skip) queryParams.set("skip", normalizedOptions.skip.toString());
|
|
2880
|
+
if (normalizedOptions.sort) {
|
|
2881
|
+
if (Array.isArray(normalizedOptions.sort) && typeof normalizedOptions.sort[0] === "object") {
|
|
2882
|
+
queryParams.set("sort", JSON.stringify(normalizedOptions.sort));
|
|
2883
|
+
} else {
|
|
2884
|
+
const sortVal = Array.isArray(normalizedOptions.sort) ? normalizedOptions.sort.join(",") : normalizedOptions.sort;
|
|
2885
|
+
queryParams.set("sort", sortVal);
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
if (normalizedOptions.select) {
|
|
2889
|
+
queryParams.set("select", normalizedOptions.select.join(","));
|
|
2890
|
+
}
|
|
2891
|
+
const filterValue = normalizedOptions.filter ?? normalizedOptions.filters;
|
|
2892
|
+
if (filterValue) {
|
|
2893
|
+
if (this.parent._isFilterAST(filterValue) || Array.isArray(filterValue)) {
|
|
2894
|
+
queryParams.set("filter", JSON.stringify(filterValue));
|
|
2895
|
+
} else if (typeof filterValue === "object" && filterValue !== null) {
|
|
2896
|
+
Object.entries(filterValue).forEach(([k, v]) => {
|
|
2897
|
+
if (v !== void 0 && v !== null) {
|
|
2898
|
+
queryParams.append(k, String(v));
|
|
2899
|
+
}
|
|
2900
|
+
});
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
if (normalizedOptions.aggregations) {
|
|
2904
|
+
queryParams.set("aggregations", JSON.stringify(normalizedOptions.aggregations));
|
|
2905
|
+
}
|
|
2906
|
+
if (normalizedOptions.groupBy) {
|
|
2907
|
+
queryParams.set("groupBy", normalizedOptions.groupBy.join(","));
|
|
2908
|
+
}
|
|
2909
|
+
const qs = queryParams.toString();
|
|
2910
|
+
const res = await this.parent._fetch(this.url(`/data/${object}${qs ? `?${qs}` : ""}`));
|
|
2911
|
+
return this.parent._unwrap(res);
|
|
2912
|
+
},
|
|
2913
|
+
get: async (object, id) => {
|
|
2914
|
+
const res = await this.parent._fetch(this.url(`/data/${object}/${id}`));
|
|
2915
|
+
return this.parent._unwrap(res);
|
|
2916
|
+
},
|
|
2917
|
+
create: async (object, data) => {
|
|
2918
|
+
const res = await this.parent._fetch(this.url(`/data/${object}`), {
|
|
2919
|
+
method: "POST",
|
|
2920
|
+
body: JSON.stringify(data)
|
|
2921
|
+
});
|
|
2922
|
+
return this.parent._unwrap(res);
|
|
2923
|
+
},
|
|
2924
|
+
createMany: async (object, data) => {
|
|
2925
|
+
const res = await this.parent._fetch(this.url(`/data/${object}/createMany`), {
|
|
2926
|
+
method: "POST",
|
|
2927
|
+
body: JSON.stringify(data)
|
|
2928
|
+
});
|
|
2929
|
+
return this.parent._unwrap(res);
|
|
2930
|
+
},
|
|
2931
|
+
update: async (object, id, data) => {
|
|
2932
|
+
const res = await this.parent._fetch(this.url(`/data/${object}/${id}`), {
|
|
2933
|
+
method: "PATCH",
|
|
2934
|
+
body: JSON.stringify(data)
|
|
2935
|
+
});
|
|
2936
|
+
return this.parent._unwrap(res);
|
|
2937
|
+
},
|
|
2938
|
+
batch: async (object, request) => {
|
|
2939
|
+
const res = await this.parent._fetch(this.url(`/data/${object}/batch`), {
|
|
2940
|
+
method: "POST",
|
|
2941
|
+
body: JSON.stringify(request)
|
|
2942
|
+
});
|
|
2943
|
+
return this.parent._unwrap(res);
|
|
2944
|
+
},
|
|
2945
|
+
updateMany: async (object, records, options) => {
|
|
2946
|
+
const request = { records, options };
|
|
2947
|
+
const res = await this.parent._fetch(this.url(`/data/${object}/updateMany`), {
|
|
2948
|
+
method: "POST",
|
|
2949
|
+
body: JSON.stringify(request)
|
|
2950
|
+
});
|
|
2951
|
+
return this.parent._unwrap(res);
|
|
2952
|
+
},
|
|
2953
|
+
delete: async (object, id) => {
|
|
2954
|
+
const res = await this.parent._fetch(this.url(`/data/${object}/${id}`), {
|
|
2955
|
+
method: "DELETE"
|
|
2956
|
+
});
|
|
2957
|
+
return this.parent._unwrap(res);
|
|
2958
|
+
},
|
|
2959
|
+
deleteMany: async (object, ids, options) => {
|
|
2960
|
+
const request = { ids, options };
|
|
2961
|
+
const res = await this.parent._fetch(this.url(`/data/${object}/deleteMany`), {
|
|
2962
|
+
method: "POST",
|
|
2963
|
+
body: JSON.stringify(request)
|
|
2964
|
+
});
|
|
2965
|
+
return this.parent._unwrap(res);
|
|
2966
|
+
}
|
|
2967
|
+
};
|
|
2968
|
+
/**
|
|
2969
|
+
* Package management scoped to this project.
|
|
2970
|
+
* Only the read-path is exposed here — publish / delete remain on the
|
|
2971
|
+
* global `client.packages` namespace for now, pending dedicated per-project
|
|
2972
|
+
* package tests.
|
|
2973
|
+
*/
|
|
2974
|
+
this.packages = {
|
|
2975
|
+
list: async () => {
|
|
2976
|
+
const res = await this.parent._fetch(this.url("/packages"));
|
|
2977
|
+
return this.parent._unwrap(res);
|
|
2978
|
+
},
|
|
2979
|
+
get: async (id, version) => {
|
|
2980
|
+
const qs = version ? `?version=${encodeURIComponent(version)}` : "";
|
|
2981
|
+
const res = await this.parent._fetch(this.url(`/packages/${encodeURIComponent(id)}${qs}`));
|
|
2982
|
+
return this.parent._unwrap(res);
|
|
2983
|
+
}
|
|
2984
|
+
};
|
|
2985
|
+
/**
|
|
2986
|
+
* Automation (Flow) operations scoped to this project.
|
|
2987
|
+
*
|
|
2988
|
+
* Thin wrapper around the dispatcher's automation routes, mounted under
|
|
2989
|
+
* `/api/v1/projects/:projectId/automation/...`. Surface mirrors the methods
|
|
2990
|
+
* needed by Studio's Flow viewer: read flow definition, execute (trigger),
|
|
2991
|
+
* list runs, fetch a single run.
|
|
2992
|
+
*/
|
|
2993
|
+
this.automation = {
|
|
2994
|
+
/** Fetch a flow definition by name. */
|
|
2995
|
+
getFlow: async (name) => {
|
|
2996
|
+
const res = await this.parent._fetch(this.url(`/automation/${encodeURIComponent(name)}`));
|
|
2997
|
+
return this.parent._unwrap(res);
|
|
2998
|
+
},
|
|
2999
|
+
/**
|
|
3000
|
+
* Execute (trigger) a flow by name. The request body is forwarded as the
|
|
3001
|
+
* automation execution context (e.g. `{ params, trigger }`).
|
|
3002
|
+
*/
|
|
3003
|
+
execute: async (name, ctx) => {
|
|
3004
|
+
const res = await this.parent._fetch(this.url(`/automation/${encodeURIComponent(name)}/trigger`), {
|
|
3005
|
+
method: "POST",
|
|
3006
|
+
body: JSON.stringify(ctx ?? {})
|
|
3007
|
+
});
|
|
3008
|
+
return this.parent._unwrap(res);
|
|
3009
|
+
},
|
|
3010
|
+
/** List recent runs for a flow. */
|
|
3011
|
+
listRuns: async (flowName, opts) => {
|
|
3012
|
+
const params = new URLSearchParams();
|
|
3013
|
+
if (opts?.limit != null) params.set("limit", String(opts.limit));
|
|
3014
|
+
if (opts?.cursor) params.set("cursor", opts.cursor);
|
|
3015
|
+
const qs = params.toString();
|
|
3016
|
+
const res = await this.parent._fetch(
|
|
3017
|
+
this.url(`/automation/${encodeURIComponent(flowName)}/runs${qs ? `?${qs}` : ""}`)
|
|
3018
|
+
);
|
|
3019
|
+
return this.parent._unwrap(res);
|
|
3020
|
+
},
|
|
3021
|
+
/** Fetch a single run (with step log) for a flow. */
|
|
3022
|
+
getRun: async (flowName, runId) => {
|
|
3023
|
+
const res = await this.parent._fetch(
|
|
3024
|
+
this.url(`/automation/${encodeURIComponent(flowName)}/runs/${encodeURIComponent(runId)}`)
|
|
3025
|
+
);
|
|
3026
|
+
return this.parent._unwrap(res);
|
|
3027
|
+
}
|
|
3028
|
+
};
|
|
3029
|
+
this.parent = parent;
|
|
3030
|
+
this.projectId = projectId;
|
|
3031
|
+
}
|
|
3032
|
+
/** The projectId this client is scoped to. */
|
|
3033
|
+
getProjectId() {
|
|
3034
|
+
return this.projectId;
|
|
3035
|
+
}
|
|
3036
|
+
/** Prefix segment inserted between the baseUrl and the resource path. */
|
|
3037
|
+
scope() {
|
|
3038
|
+
return `/api/v1/projects/${encodeURIComponent(this.projectId)}`;
|
|
3039
|
+
}
|
|
3040
|
+
url(suffix) {
|
|
3041
|
+
return `${this.parent._baseUrl()}${this.scope()}${suffix}`;
|
|
3042
|
+
}
|
|
3043
|
+
};
|
|
1781
3044
|
export {
|
|
1782
3045
|
FilterBuilder,
|
|
1783
3046
|
ObjectStackClient,
|
|
1784
3047
|
QueryBuilder,
|
|
1785
3048
|
RealtimeAPI,
|
|
3049
|
+
ScopedProjectClient,
|
|
1786
3050
|
createFilter,
|
|
1787
3051
|
createQuery
|
|
1788
3052
|
};
|