Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions apps/cloud/src/services/ids.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, expect, it } from "@effect/vitest";

import {
newId,
orgScopeId,
slugifyHandle,
userOrgScopeId,
userWorkspaceScopeId,
withHandleSuffix,
workspaceScopeId,
} from "./ids";

describe("newId", () => {
it("emits prefix + base58 body", () => {
const id = newId("workspace");
expect(id.startsWith("workspace_")).toBe(true);
const body = id.slice("workspace_".length);
expect(body).toMatch(/^[1-9A-HJ-NP-Za-km-z]{22}$/);
});

it("collides with negligible probability across 1k draws", () => {
const seen = new Set<string>();
for (let i = 0; i < 1000; i++) seen.add(newId("workspace"));
expect(seen.size).toBe(1000);
});
});

describe("scope id constructors", () => {
it("orgScopeId formats org_<id>", () => {
expect(orgScopeId("org_abc").toString()).toBe("org_org_abc");
expect(orgScopeId("01H").toString()).toBe("org_01H");
});

it("workspaceScopeId formats workspace_<id>", () => {
expect(workspaceScopeId("ws_abc").toString()).toBe("workspace_ws_abc");
});

it("userOrgScopeId formats user_org_<userId>_<orgId>", () => {
expect(userOrgScopeId("u1", "o1").toString()).toBe("user_org_u1_o1");
});

it("userWorkspaceScopeId formats user_workspace_<userId>_<workspaceId>", () => {
expect(userWorkspaceScopeId("u1", "w1").toString()).toBe(
"user_workspace_u1_w1",
);
});
});

describe("slugifyHandle", () => {
it("lowercases and hyphenates", () => {
expect(slugifyHandle("Acme Corp")).toBe("acme-corp");
});

it("collapses runs and trims edges", () => {
expect(slugifyHandle(" --Acme!! Corp__ ")).toBe("acme-corp");
});

it("strips diacritics", () => {
expect(slugifyHandle("Café Münchën")).toBe("cafe-munchen");
});

it("falls back to 'org' for empty results", () => {
expect(slugifyHandle("!!!")).toBe("org");
expect(slugifyHandle("")).toBe("org");
});

it("caps length at 48", () => {
const long = "a".repeat(120);
expect(slugifyHandle(long).length).toBe(48);
});
});

describe("withHandleSuffix", () => {
it("appends -n", () => {
expect(withHandleSuffix("acme", 2)).toBe("acme-2");
expect(withHandleSuffix("acme", 17)).toBe("acme-17");
});

it("keeps total length <= 48 by truncating the base", () => {
const base = "a".repeat(48);
const out = withHandleSuffix(base, 9);
expect(out.length).toBe(48);
expect(out.endsWith("-9")).toBe(true);
});
});
88 changes: 88 additions & 0 deletions apps/cloud/src/services/ids.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// ---------------------------------------------------------------------------
// Cloud id helpers
// ---------------------------------------------------------------------------
//
// Two flavors:
//
// - `newId(prefix)` — random, prefixed local id (Unkey-style). Used for
// entities the cloud owns (workspaces, future local orgs, …). WorkOS
// ids stay as identity anchors; we don't re-prefix them.
//
// - `orgScopeId / workspaceScopeId / userOrgScopeId / userWorkspaceScopeId`
// — deterministic scope id constructors. Scope rows are addressed by
// these strings; the prefixes make a row's owner trivially inspectable
// and prevent accidental collisions between user scopes and org scopes.
//
// Plus `slugifyHandle / withHandleSuffix` for generating org handles and
// workspace slugs from human-entered names.

import { ScopeId } from "@executor-js/sdk";

const BASE58_ALPHABET =
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";

const randomBase58 = (length: number): string => {
const bytes = new Uint8Array(length);
crypto.getRandomValues(bytes);
let out = "";
for (let i = 0; i < length; i++) {
out += BASE58_ALPHABET[bytes[i]! % 58];
}
return out;
};

/**
* Random prefixed id, ~128 bits of entropy. Output shape: `${prefix}_<22 base58>`.
*/
export const newId = (prefix: string): string => `${prefix}_${randomBase58(22)}`;

// ---------------------------------------------------------------------------
// Deterministic scope id constructors
// ---------------------------------------------------------------------------

export const orgScopeId = (orgId: string): ScopeId =>
ScopeId.make(`org_${orgId}`);

export const workspaceScopeId = (workspaceId: string): ScopeId =>
ScopeId.make(`workspace_${workspaceId}`);

export const userOrgScopeId = (userId: string, orgId: string): ScopeId =>
Comment on lines +43 to +49
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

branded types to org scope id, workspace scope id

ScopeId.make(`user_org_${userId}_${orgId}`);

export const userWorkspaceScopeId = (
userId: string,
workspaceId: string,
): ScopeId => ScopeId.make(`user_workspace_${userId}_${workspaceId}`);

// ---------------------------------------------------------------------------
// Handle / slug helpers
// ---------------------------------------------------------------------------

const HANDLE_MAX = 48;

/**
* Reduce a free-form name to a handle/slug. Lowercase, ASCII-ish, hyphenated.
* Caller is responsible for collision handling — see `withHandleSuffix`.
*/
export const slugifyHandle = (name: string): string => {
const cleaned = name
.normalize("NFKD")
.replace(/[̀-ͯ]/g, "")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.replace(/-{2,}/g, "-")
.slice(0, HANDLE_MAX);
return cleaned.length > 0 ? cleaned : "org";
};

/**
* Append a numeric suffix to a handle, keeping the result within HANDLE_MAX.
* `withHandleSuffix("acme", 2)` → `"acme-2"`.
*/
export const withHandleSuffix = (handle: string, n: number): string => {
const suffix = `-${n}`;
const room = HANDLE_MAX - suffix.length;
const base = handle.slice(0, Math.max(1, room));
return `${base}${suffix}`;
};
Loading