@stigmer/server 3.37.1 → 3.38.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.
Files changed (33) hide show
  1. package/dist/boot/compose.d.ts.map +1 -1
  2. package/dist/boot/compose.js +3 -0
  3. package/dist/boot/compose.js.map +1 -1
  4. package/dist/domain/iampolicy/grant-path.d.ts +7 -3
  5. package/dist/domain/iampolicy/grant-path.d.ts.map +1 -1
  6. package/dist/domain/iampolicy/grant-path.js +10 -2
  7. package/dist/domain/iampolicy/grant-path.js.map +1 -1
  8. package/dist/domain/organization/controller.d.ts +4 -1
  9. package/dist/domain/organization/controller.d.ts.map +1 -1
  10. package/dist/domain/organization/controller.js +28 -4
  11. package/dist/domain/organization/controller.js.map +1 -1
  12. package/dist/domain/organization/steps.d.ts +22 -0
  13. package/dist/domain/organization/steps.d.ts.map +1 -1
  14. package/dist/domain/organization/steps.js +50 -2
  15. package/dist/domain/organization/steps.js.map +1 -1
  16. package/dist/extensions/gate-slots.d.ts +14 -1
  17. package/dist/extensions/gate-slots.d.ts.map +1 -1
  18. package/dist/extensions/gate-slots.js +1 -0
  19. package/dist/extensions/gate-slots.js.map +1 -1
  20. package/dist/pipeline/steps/authorization-tuples.d.ts +6 -2
  21. package/dist/pipeline/steps/authorization-tuples.d.ts.map +1 -1
  22. package/dist/pipeline/steps/authorization-tuples.js +6 -2
  23. package/dist/pipeline/steps/authorization-tuples.js.map +1 -1
  24. package/package.json +6 -6
  25. package/src/boot/compose.ts +3 -0
  26. package/src/domain/iampolicy/__tests__/grant-path.test.ts +40 -7
  27. package/src/domain/iampolicy/grant-path.ts +19 -5
  28. package/src/domain/organization/__tests__/organization-delete.test.ts +266 -0
  29. package/src/domain/organization/controller.ts +40 -8
  30. package/src/domain/organization/steps.ts +66 -2
  31. package/src/extensions/__tests__/registry.test.ts +3 -0
  32. package/src/extensions/gate-slots.ts +14 -0
  33. package/src/pipeline/steps/authorization-tuples.ts +6 -2
@@ -10,16 +10,28 @@
10
10
  * uniqueness guarantee, mirroring cloud's OrganizationCreateHandler
11
11
  * (CheckDuplicate + CopySlugToId) step-for-step.
12
12
  *
13
- * Proven by organization.conformance.test.ts (CONFORMANCE_TARGET=local)
14
- * and __tests__/organization.test.ts.
13
+ * The same deviation shapes the delete: a freed slug is anyone's to take,
14
+ * so newRevokeOrganizationPoliciesStep revokes the organization's policy
15
+ * rows BEFORE its row is deleted, and fails the delete when it cannot.
16
+ *
17
+ * Proven by organization.conformance.test.ts (CONFORMANCE_TARGET=local),
18
+ * __tests__/organization.test.ts and __tests__/organization-delete.test.ts.
15
19
  */
20
+ import { create } from "@bufbuild/protobuf";
21
+ import type { DescMessage } from "@bufbuild/protobuf";
22
+
16
23
  import { ResourceNotFoundError } from "../../store/interface.js";
17
24
  import type { Store } from "../../store/interface.js";
18
25
  import { alreadyExistsError, internalError } from "../../pipeline/errors.js";
19
26
  import type { PipelineStep } from "../../pipeline/pipeline.js";
20
27
  import type { RequestContext } from "../../pipeline/request-context.js";
28
+ import { EXISTING_RESOURCE_KEY } from "../../pipeline/steps/load-existing.js";
21
29
  import { metadataOf } from "../../pipeline/steps/shapes.js";
30
+ import type { IamPolicyGrantPath } from "../iampolicy/grant-path.js";
22
31
 
32
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
33
+ import { ApiResourceRefSchema } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/spec_pb";
34
+ import type { Organization } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
23
35
  import { OrganizationSchema } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
24
36
 
25
37
  /**
@@ -98,3 +110,55 @@ export function newCopySlugToIdStep(): PipelineStep<typeof OrganizationSchema> {
98
110
  },
99
111
  };
100
112
  }
113
+
114
+ /**
115
+ * Revokes every policy row that names the organization, as principal or as
116
+ * resource, before the delete removes its row: the rows its members hold
117
+ * on it, and the scope link every organization-scoped resource holds to
118
+ * it. Runs after the `org-delete:pre-delete` slot and before
119
+ * DeleteResource.
120
+ *
121
+ * The generic delete chains clean up after the row and log a fault,
122
+ * because a deleted resource's rows grant nothing once it is gone. An
123
+ * organization is the exception: its id is its slug, the delete frees the
124
+ * slug, and a row left behind would grant whoever creates the slug next.
125
+ * So this step does not catch. A fault fails the delete with the
126
+ * organization in place, and a retry resumes where the revocation stopped
127
+ * (the grant path revokes the organization's owners last, so the owner
128
+ * who retries still can). The grant path fires the composed driver's
129
+ * `onPolicyRevoked` before each row, so a composition's tuples go with
130
+ * their rows. The post-delete CleanupIamPolicies still runs: it catches
131
+ * whatever a concurrent write named the organization with in between.
132
+ */
133
+ export function newRevokeOrganizationPoliciesStep<Desc extends DescMessage>(
134
+ grantPath: IamPolicyGrantPath,
135
+ ): PipelineStep<Desc> {
136
+ return {
137
+ name: "RevokeOrganizationPolicies",
138
+ async execute(ctx: RequestContext<Desc>): Promise<void> {
139
+ const organization = ctx.get(EXISTING_RESOURCE_KEY) as
140
+ | Organization
141
+ | undefined;
142
+ const id = organization?.metadata?.id ?? "";
143
+ if (id === "") {
144
+ throw internalError(
145
+ new Error("organization delete reached RevokeOrganizationPolicies without its loaded row"),
146
+ "failed to remove the organization's access policies",
147
+ );
148
+ }
149
+ try {
150
+ await grantPath.cleanupResource(
151
+ create(ApiResourceRefSchema, {
152
+ kind: ApiResourceKind[ApiResourceKind.organization],
153
+ id,
154
+ }),
155
+ );
156
+ } catch (error) {
157
+ throw internalError(
158
+ error,
159
+ "failed to remove the organization's access policies",
160
+ );
161
+ }
162
+ },
163
+ };
164
+ }
@@ -491,6 +491,9 @@ describe("resolveExtensions — loud-fail throws (DD-006 §2b)", () => {
491
491
  // The ninth: the organization create chain before Persist, where a
492
492
  // limit on which organizations may exist refuses with nothing written.
493
493
  "org-create:pre-side-effect-gate",
494
+ // The tenth: the organization delete chain before any write, where an
495
+ // edition removes or refuses what it keeps for the organization.
496
+ "org-delete:pre-delete",
494
497
  "sandbox-acquisition:gate",
495
498
  "session-create:pre-side-effect-gate",
496
499
  ]);
@@ -53,6 +53,19 @@
53
53
  * limit belongs here too. `apply` delegates to create on its create arm,
54
54
  * so the slot fires there as well.
55
55
  *
56
+ * The tenth, `org-delete:pre-delete`: the organization delete chain after
57
+ * LoadExistingForDelete and before any write, so the organization exists
58
+ * and is loaded (EXISTING_RESOURCE_KEY) while its steps run. An
59
+ * organization's id is its slug and its delete frees the slug for anyone,
60
+ * so whatever an edition keeps for the organization must go before the row
61
+ * does, or be refused: a row left behind would belong to whoever creates
62
+ * the slug next. Its steps may refuse (Enterprise refuses while an
63
+ * identity provider still signs in platform-managed organizations) or
64
+ * remove the edition's own rows; either way a throw fails the delete with
65
+ * the organization intact, and the retry re-runs every step, so each owns
66
+ * its idempotency. After the slot the chain revokes the organization's own
67
+ * policy rows, also before the row and also failing closed.
68
+ *
56
69
  * Two enforcement layers, deliberately redundant, both derived from the
57
70
  * ONE literal tuple below (lockstep by construction):
58
71
  * - GateSlotName (compile time): the union of declared slot literals.
@@ -92,6 +105,7 @@ export const GATE_SLOT_NAMES = [
92
105
  "identity-account-provision:post-persist",
93
106
  "iam-policy-create:pre-side-effect-gate",
94
107
  "org-create:pre-side-effect-gate",
108
+ "org-delete:pre-delete",
95
109
  ] as const;
96
110
 
97
111
  /** The declared slot-name union — a registration outside it fails tsc. */
@@ -330,8 +330,12 @@ export function newCreateAuthorizationTuplesStep<Desc extends DescMessage>(
330
330
  /**
331
331
  * CleanupIamPolicies — after the store delete in every delete chain.
332
332
  * Best-effort by contract: a driver failure is logged and the delete
333
- * succeeds (Java's DeleteOperationCleanupIamPoliciesStep swallows all —
334
- * the cloud side owns convergence for orphaned grants).
333
+ * succeeds (Java's DeleteOperationCleanupIamPoliciesStep swallows all),
334
+ * because a deleted resource's rows grant nothing once it is gone. The
335
+ * organization is the one kind whose id another caller can take next, so
336
+ * its chain revokes the organization's rows before the row, failing
337
+ * closed (RevokeOrganizationPolicies, domain/organization/steps.ts), and
338
+ * this step is only its backstop there.
335
339
  */
336
340
  export function newCleanupIamPoliciesStep<Desc extends DescMessage>(
337
341
  lifecycle: ResourceAuthorizationLifecycle | undefined,