@antelopejs/dms-saas 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -32,6 +32,34 @@ workspace. From that directory run `pnpm install`,
32
32
  `pnpm test:frontend`. The frontend check generates a temporary workspace, then
33
33
  builds client, SSR, and email bundles and runs Vue typechecking.
34
34
 
35
+ ## Completing a registration that entered without a workspace
36
+
37
+ An account can reach the product before it owns anything to log into: an OAuth
38
+ sign-in for an unknown e-mail, or any entry point the core answers with
39
+ `requires_tenant_assignment`. The `auth/no-workspace` page is where that
40
+ account buys its way in — a workspace, a plan, a card — and
41
+ `POST /api/saas/register/finalize` provisions it and answers with a token pair
42
+ for the workspace it just created, so the visitor lands signed in instead of
43
+ back on the login screen.
44
+
45
+ The page never touches those tokens. It posts the call through the frontend
46
+ loader's generic `POST /auth/establish` route, naming the endpoint to call and
47
+ its payload; the loader calls `/api/saas/register/finalize` itself over its own
48
+ server-to-server channel to the DMS API and writes its session cookie from what
49
+ comes back. Because that route turns a backend endpoint into a login, it only
50
+ calls endpoints the deployment named, so a project serving this page must
51
+ declare the finalize endpoint in the frontend server's environment:
52
+
53
+ ```bash
54
+ # .env of the frontend server
55
+ DMS_AUTH_ESTABLISH_ENDPOINTS=/api/saas/register/finalize
56
+ ```
57
+
58
+ Without it the loader answers `403` and a visitor who has paid stays signed
59
+ out. A replacement completion screen keeps the same requirement, and can build
60
+ its request with the auto-imported `buildSessionEstablishRequest()` helper
61
+ rather than restating the endpoint and the payload shape.
62
+
35
63
  ## Tenant owner permissions
36
64
 
37
65
  The `dms-saas.plan-intersection` permissions resolver runs at order 100. For a
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Completing a self-service registration that entered without a workspace.
3
+ *
4
+ * The account already exists — it signed in through OAuth, or through any
5
+ * entry point that ends on a tenant assignment token — and everything that is
6
+ * still missing is bought here: a workspace, a plan, a card. The API answers
7
+ * with a session for the workspace it just provisioned, so the visitor lands
8
+ * signed in rather than back on the login screen.
9
+ *
10
+ * That session is opened by the frontend server, never in the browser: the
11
+ * page asks the loader's `/auth/establish` route to call the finalize endpoint
12
+ * on its behalf, and the loader writes its session cookie from the token pair
13
+ * the API returns. No access or refresh token is ever handed to the page.
14
+ *
15
+ * The deployment must name the finalize endpoint in the frontend server's
16
+ * `DMS_AUTH_ESTABLISH_ENDPOINTS`, otherwise `/auth/establish` answers 403:
17
+ * turning a backend route into a login is the operator's decision, not a
18
+ * module's.
19
+ */
20
+
21
+ /** Loader route that opens a session from a backend-issued token pair. */
22
+ export const SESSION_ESTABLISH_ENDPOINT = "/auth/establish";
23
+
24
+ /** DMS API route that provisions the workspace and mints that token pair. */
25
+ export const WORKSPACE_FINALIZE_ENDPOINT = "/api/saas/register/finalize";
26
+
27
+ export interface WorkspaceFinalizationAddress {
28
+ country: string;
29
+ line1?: string;
30
+ postalCode?: string;
31
+ city?: string;
32
+ }
33
+
34
+ /** The completion form as the visitor left it, plus the confirmed card. */
35
+ export interface WorkspaceFinalization {
36
+ tenantAssignmentToken: string;
37
+ workspaceName: string;
38
+ planId: string;
39
+ customerType: "individual" | "business";
40
+ companyName: string;
41
+ vatNumber: string;
42
+ address: WorkspaceFinalizationAddress;
43
+ paymentMethodId: string;
44
+ }
45
+
46
+ export interface WorkspaceFinalizationPayload {
47
+ tenant_assignment_token: string;
48
+ workspaceName: string;
49
+ planId: string;
50
+ customerType: "individual" | "business";
51
+ companyName?: string;
52
+ vatNumber?: string;
53
+ address: WorkspaceFinalizationAddress;
54
+ paymentMethodId: string;
55
+ }
56
+
57
+ /** What `/auth/establish` takes: an endpoint to call, and its body. */
58
+ export interface SessionEstablishRequest {
59
+ endpoint: string;
60
+ payload: WorkspaceFinalizationPayload;
61
+ }
62
+
63
+ /**
64
+ * The finalize call, addressed to the frontend server instead of the API.
65
+ *
66
+ * Built in one place because two things have to stay true together: the
67
+ * payload is what `/api/saas/register/finalize` validates, and the endpoint
68
+ * named in the envelope is the one the operator allowed — a page that inlined
69
+ * either would drift from the other.
70
+ *
71
+ * Company fields only travel for a business customer: an individual who typed
72
+ * a company name and switched back would otherwise be invoiced as one.
73
+ *
74
+ * @param finalization Completion form and confirmed payment method
75
+ * @returns Body to POST to `/auth/establish`
76
+ */
77
+ export function buildSessionEstablishRequest(
78
+ finalization: WorkspaceFinalization,
79
+ ): SessionEstablishRequest {
80
+ const isBusiness = finalization.customerType === "business";
81
+ return {
82
+ endpoint: WORKSPACE_FINALIZE_ENDPOINT,
83
+ payload: {
84
+ tenant_assignment_token: finalization.tenantAssignmentToken,
85
+ workspaceName: finalization.workspaceName,
86
+ planId: finalization.planId,
87
+ customerType: finalization.customerType,
88
+ companyName: isBusiness ? finalization.companyName : undefined,
89
+ vatNumber: isBusiness ? finalization.vatNumber : undefined,
90
+ address: finalization.address,
91
+ paymentMethodId: finalization.paymentMethodId,
92
+ },
93
+ };
94
+ }
@@ -39,7 +39,6 @@ interface PendingRegistration {
39
39
  const PLANS_ENDPOINT = "/api/saas/plans/public";
40
40
  const SETUP_INTENT_ENDPOINT = "/api/saas/register/setup-intent";
41
41
  const PENDING_ENDPOINT = "/api/saas/register/pending";
42
- const FINALIZE_ENDPOINT = "/auth/finalize-workspace";
43
42
  const LOGIN_REDIRECT = "/auth/login";
44
43
  const PAYMENT_ELEMENT_ID = "dms-saas-finalize-payment-element";
45
44
  const HOME_REDIRECT = "/";
@@ -68,8 +67,9 @@ const stripePublishableKey = computed<string>(
68
67
  () => dmsSaasRuntime.value?.stripePublishableKey ?? "",
69
68
  );
70
69
 
71
- // Only `finalize` goes through the frontend server because it opens the session
72
- // cookie; all other public registration endpoints live on the DMS API.
70
+ // Only `finalize` goes through the frontend server, and only because it opens
71
+ // the session cookie on the workspace it provisions; every other public
72
+ // registration endpoint is called on the DMS API directly.
73
73
  const apiFetch = $fetch.create({ baseURL: dmsRuntime.value.baseURL });
74
74
 
75
75
  const { countryItems } = useBillingCountries();
@@ -225,17 +225,15 @@ async function submit(): Promise<void> {
225
225
  nuxtApp.$i18n.t("saas.register.error.no_payment");
226
226
  return;
227
227
  }
228
- await $fetch(FINALIZE_ENDPOINT, {
228
+ await $fetch(SESSION_ESTABLISH_ENDPOINT, {
229
229
  method: "POST",
230
- body: {
231
- tenant_assignment_token: tenantAssignmentToken.value,
230
+ body: buildSessionEstablishRequest({
231
+ tenantAssignmentToken: tenantAssignmentToken.value,
232
232
  workspaceName: workspaceName.value,
233
233
  planId: selectedPlanId.value,
234
234
  customerType: customerType.value,
235
- companyName:
236
- customerType.value === "business" ? companyName.value : undefined,
237
- vatNumber:
238
- customerType.value === "business" ? vatNumber.value : undefined,
235
+ companyName: companyName.value,
236
+ vatNumber: vatNumber.value,
239
237
  address: {
240
238
  country: country.value,
241
239
  line1: addressLine1.value || undefined,
@@ -243,7 +241,7 @@ async function submit(): Promise<void> {
243
241
  city: city.value || undefined,
244
242
  },
245
243
  paymentMethodId: confirm.paymentMethodId,
246
- },
244
+ }),
247
245
  });
248
246
  if (typeof window !== "undefined") {
249
247
  window.location.href = HOME_REDIRECT;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms-saas",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,7 +31,7 @@
31
31
  "@antelopejs/dms": ">=0.0.1 <1.0.0",
32
32
  "@antelopejs/interface-api": ">=0.0.13 <1.0.0",
33
33
  "@antelopejs/interface-api-util": ">=0.1.2 <1.0.0",
34
- "@antelopejs/interface-core": "^0.0.12",
34
+ "@antelopejs/interface-core": ">=0.0.13 <1.0.0",
35
35
  "@antelopejs/interface-data-api": ">=0.1.9 <1.0.0",
36
36
  "@antelopejs/interface-database": ">=0.1.7 <1.0.0",
37
37
  "@antelopejs/interface-database-decorators": ">=0.1.6 <1.0.0",
@@ -47,7 +47,7 @@
47
47
  "@antelopejs/dms-frontend": ">=0.0.1 <1.0.0",
48
48
  "@antelopejs/file-storage-local": "0.1.4",
49
49
  "@antelopejs/mongodb": "1.3.0",
50
- "@antelopejs/tooling-configs": "^0.0.4",
50
+ "@antelopejs/tooling-configs": ">=0.0.6 <1.0.0",
51
51
  "@types/node": "^24.0.0",
52
52
  "@types/node-cron": "^3.0.11",
53
53
  "knip": "^6.34.0",