@open-mercato/shared 0.6.6 → 0.6.7-develop.6552.1.5208a19369

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/AGENTS.md CHANGED
@@ -52,6 +52,7 @@ yarn workspace @open-mercato/shared build
52
52
  | `number.ts` | When parsing numeric strings from env/query params with a fallback and optional min/integer constraint | `@open-mercato/shared/lib/number` |
53
53
  | `openapi/` | When generating CRUD OpenAPI specs | `@open-mercato/shared/lib/openapi/crud` |
54
54
  | `profiler/` | When profiling with `OM_PROFILE` env flag | `@open-mercato/shared/lib/profiler` |
55
+ | `string.ts` | When parsing comma-separated lists from CLI args/query params, or coercing a string to `undefined` when blank | `@open-mercato/shared/lib/string` |
55
56
  | `testing/` | When bootstrapping tests — register only what the test needs | `@open-mercato/shared/lib/testing/bootstrap` |
56
57
 
57
58
  ## Module Types (`src/modules/`)
@@ -94,6 +95,23 @@ const results = await findWithDecryption(em, 'Entity', filter, { tenantId, organ
94
95
  import { parseBooleanToken, parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
95
96
  ```
96
97
 
98
+ ### Comma-Separated List Parsing — MUST use instead of ad-hoc splitting
99
+
100
+ ```typescript
101
+ import { parseCommaSeparatedList } from '@open-mercato/shared/lib/string'
102
+ ```
103
+
104
+ `parseCommaSeparatedList(value)` splits on commas, trims each entry, and drops blanks. Non-string
105
+ inputs (`null`/`undefined`) yield `[]`. MUST use it instead of hand-rolling
106
+ `value.split(',').map((s) => s.trim()).filter(Boolean)` when reading CLI flags or query params.
107
+
108
+ Keep the surrounding guard when a call site distinguishes "not supplied" (`undefined`) from
109
+ "supplied but empty" (`[]`) — the helper always returns an array:
110
+
111
+ ```typescript
112
+ const roleNames = rolesCsv ? parseCommaSeparatedList(rolesCsv) : undefined
113
+ ```
114
+
97
115
  ### Browser Storage — use the shared helpers instead of raw `localStorage`
98
116
 
99
117
  Persisted client UI state MUST go through the shared `browser/` helpers, never raw `window.localStorage` reads/writes scattered per component:
@@ -264,6 +264,8 @@ function json(data, init) {
264
264
  headers: { "content-type": "application/json", ...init?.headers || {} }
265
265
  });
266
266
  }
267
+ const SELECTED_ORG_COOKIE = "om_selected_org";
268
+ const CLEAR_SELECTED_ORG_COOKIE = `${SELECTED_ORG_COOKIE}=; Path=/; Max-Age=0; SameSite=Lax`;
267
269
  function attachOperationHeader(res, logEntry) {
268
270
  if (!res || !(res instanceof Response)) return res;
269
271
  if (!logEntry || typeof logEntry !== "object") return res;
@@ -892,6 +894,26 @@ function makeCrudRoute(opts) {
892
894
  }
893
895
  return { container, auth: scopedAuth, organizationScope: scope, selectedOrganizationId, organizationIds, request };
894
896
  }
897
+ function rejectInvalidOrgSelection(ctx, action) {
898
+ if (!ormCfg.orgField) return null;
899
+ if (!ctx.organizationScope?.selectionRejected) return null;
900
+ logForbidden({
901
+ resourceKind,
902
+ action,
903
+ reason: "organization_selection_invalid",
904
+ userId: ctx.auth?.sub ?? null,
905
+ tenantId: ctx.auth?.tenantId ?? null,
906
+ organizationIds: ctx.organizationIds
907
+ });
908
+ const headers = action === "list" ? { "set-cookie": CLEAR_SELECTED_ORG_COOKIE } : {};
909
+ return json(
910
+ {
911
+ error: "Your selected organization is no longer available. Please re-select an organization and try again.",
912
+ code: "organization_selection_invalid"
913
+ },
914
+ { status: 422, headers }
915
+ );
916
+ }
895
917
  async function GET(request) {
896
918
  const profiler = createCrudProfiler(resourceKind, "list");
897
919
  const requestMeta = { method: request.method };
@@ -919,6 +941,11 @@ function makeCrudRoute(opts) {
919
941
  finishProfile({ reason: "unauthorized" });
920
942
  return json({ error: "Unauthorized" }, { status: 401 });
921
943
  }
944
+ const listSelectionRejected = rejectInvalidOrgSelection(ctx, "list");
945
+ if (listSelectionRejected) {
946
+ finishProfile({ reason: "organization_selection_invalid" });
947
+ return listSelectionRejected;
948
+ }
922
949
  if (!opts.list) {
923
950
  finishProfile({ reason: "list_not_configured" });
924
951
  return json({ error: "Not implemented" }, { status: 501 });
@@ -1510,6 +1537,8 @@ function makeCrudRoute(opts) {
1510
1537
  if (!opts.create && !useCommand) return json({ error: "Not implemented" }, { status: 501 });
1511
1538
  const ctx = await withCtx(request);
1512
1539
  if (!ctx.auth) return json({ error: "Unauthorized" }, { status: 401 });
1540
+ const createSelectionRejected = rejectInvalidOrgSelection(ctx, "create");
1541
+ if (createSelectionRejected) return createSelectionRejected;
1513
1542
  if (ormCfg.orgField && ctx.organizationIds && ctx.organizationIds.length === 0) {
1514
1543
  logForbidden({
1515
1544
  resourceKind,
@@ -1753,6 +1782,8 @@ function makeCrudRoute(opts) {
1753
1782
  if (!opts.update && !useCommand) return json({ error: "Not implemented" }, { status: 501 });
1754
1783
  const ctx = await withCtx(request);
1755
1784
  if (!ctx.auth) return json({ error: "Unauthorized" }, { status: 401 });
1785
+ const updateSelectionRejected = rejectInvalidOrgSelection(ctx, "update");
1786
+ if (updateSelectionRejected) return updateSelectionRejected;
1756
1787
  if (ormCfg.orgField && ctx.organizationIds && ctx.organizationIds.length === 0) {
1757
1788
  logForbidden({
1758
1789
  resourceKind,
@@ -2053,6 +2084,8 @@ function makeCrudRoute(opts) {
2053
2084
  try {
2054
2085
  const ctx = await withCtx(request);
2055
2086
  if (!ctx.auth) return json({ error: "Unauthorized" }, { status: 401 });
2087
+ const deleteSelectionRejected = rejectInvalidOrgSelection(ctx, "delete");
2088
+ if (deleteSelectionRejected) return deleteSelectionRejected;
2056
2089
  if (ormCfg.orgField && ctx.organizationIds && ctx.organizationIds.length === 0) {
2057
2090
  logForbidden({
2058
2091
  resourceKind,