@selvajs/platform 0.14.3-beta.0 → 0.15.0-beta.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/LICENSE +1 -1
- package/dist/computeServer/index.d.ts +1 -1
- package/dist/computeServer/index.d.ts.map +1 -1
- package/dist/computeServer/interface.d.ts +29 -1
- package/dist/computeServer/interface.d.ts.map +1 -1
- package/dist/computeServer/types.d.ts +12 -1
- package/dist/computeServer/types.d.ts.map +1 -1
- package/dist/computeServer/types.js.map +1 -1
- package/dist/data/index.d.ts +2 -1
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +1 -1
- package/dist/data/index.js.map +1 -1
- package/dist/data/interface.d.ts +69 -8
- package/dist/data/interface.d.ts.map +1 -1
- package/dist/data/interface.js +7 -1
- package/dist/data/interface.js.map +1 -1
- package/dist/definitions/interface.d.ts +16 -1
- package/dist/definitions/interface.d.ts.map +1 -1
- package/dist/definitions/schemas.d.ts +1 -0
- package/dist/definitions/schemas.d.ts.map +1 -1
- package/dist/definitions/schemas.js +8 -0
- package/dist/definitions/schemas.js.map +1 -1
- package/dist/definitions/types.d.ts +24 -1
- package/dist/definitions/types.d.ts.map +1 -1
- package/dist/events/index.d.ts +1 -1
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +1 -1
- package/dist/events/index.js.map +1 -1
- package/dist/events/interface.d.ts +30 -0
- package/dist/events/interface.d.ts.map +1 -1
- package/dist/events/interface.js +7 -0
- package/dist/events/interface.js.map +1 -1
- package/dist/index.d.ts +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/logging/interface.d.ts +82 -0
- package/dist/logging/interface.d.ts.map +1 -0
- package/dist/logging/interface.js +33 -0
- package/dist/logging/interface.js.map +1 -0
- package/dist/metrics/interface.d.ts +6 -0
- package/dist/metrics/interface.d.ts.map +1 -1
- package/dist/metrics/interface.js.map +1 -1
- package/dist/organizations/__tests__/slug-schema.test.d.ts +2 -0
- package/dist/organizations/__tests__/slug-schema.test.d.ts.map +1 -0
- package/dist/organizations/__tests__/slug-schema.test.js +38 -0
- package/dist/organizations/__tests__/slug-schema.test.js.map +1 -0
- package/dist/organizations/index.d.ts +1 -1
- package/dist/organizations/index.d.ts.map +1 -1
- package/dist/organizations/index.js +1 -1
- package/dist/organizations/index.js.map +1 -1
- package/dist/organizations/schemas.d.ts +16 -2
- package/dist/organizations/schemas.d.ts.map +1 -1
- package/dist/organizations/schemas.js +32 -2
- package/dist/organizations/schemas.js.map +1 -1
- package/dist/solveCache/interface.d.ts +86 -0
- package/dist/solveCache/interface.d.ts.map +1 -0
- package/dist/solveCache/interface.js +12 -0
- package/dist/solveCache/interface.js.map +1 -0
- package/dist/testing/suites/definitionStoreSuite.d.ts.map +1 -1
- package/dist/testing/suites/definitionStoreSuite.js +57 -0
- package/dist/testing/suites/definitionStoreSuite.js.map +1 -1
- package/dist/testing/suites/eventSinkSuite.js +1 -0
- package/dist/testing/suites/eventSinkSuite.js.map +1 -1
- package/dist/testing/suites/platformPermissionStoreSuite.d.ts +9 -0
- package/dist/testing/suites/platformPermissionStoreSuite.d.ts.map +1 -1
- package/dist/testing/suites/platformPermissionStoreSuite.js +34 -1
- package/dist/testing/suites/platformPermissionStoreSuite.js.map +1 -1
- package/dist/testing/suites/solveMetricSinkSuite.d.ts +5 -0
- package/dist/testing/suites/solveMetricSinkSuite.d.ts.map +1 -1
- package/dist/testing/suites/solveMetricSinkSuite.js +17 -8
- package/dist/testing/suites/solveMetricSinkSuite.js.map +1 -1
- package/dist/testing/suites/storageProviderSuite.d.ts.map +1 -1
- package/dist/testing/suites/storageProviderSuite.js +23 -2
- package/dist/testing/suites/storageProviderSuite.js.map +1 -1
- package/package.json +9 -24
- package/src/access/index.ts +0 -23
- package/src/access/rules.ts +0 -276
- package/src/auth/index.ts +0 -8
- package/src/auth/interface.ts +0 -279
- package/src/auth/types.ts +0 -32
- package/src/bindings/index.ts +0 -2
- package/src/bindings/interface.ts +0 -61
- package/src/computeServer/index.ts +0 -28
- package/src/computeServer/interface.ts +0 -73
- package/src/computeServer/secrets.ts +0 -102
- package/src/computeServer/types.ts +0 -74
- package/src/computeServer/utils.ts +0 -112
- package/src/config.ts +0 -99
- package/src/context.ts +0 -80
- package/src/data/index.ts +0 -1
- package/src/data/interface.ts +0 -70
- package/src/definitions/index.ts +0 -26
- package/src/definitions/interface.ts +0 -87
- package/src/definitions/paths.ts +0 -31
- package/src/definitions/schemas.ts +0 -64
- package/src/definitions/types.ts +0 -111
- package/src/errors/interface.ts +0 -45
- package/src/errors.ts +0 -16
- package/src/events/audit.ts +0 -67
- package/src/events/index.ts +0 -9
- package/src/events/interface.ts +0 -102
- package/src/index.ts +0 -247
- package/src/invites/index.ts +0 -2
- package/src/invites/interface.ts +0 -38
- package/src/invites/types.ts +0 -39
- package/src/metrics/interface.ts +0 -86
- package/src/organizations/index.ts +0 -23
- package/src/organizations/interface.ts +0 -68
- package/src/organizations/paths.ts +0 -45
- package/src/organizations/schemas.ts +0 -61
- package/src/organizations/types.ts +0 -41
- package/src/pagination.ts +0 -45
- package/src/permissions/index.ts +0 -3
- package/src/permissions/interface.ts +0 -60
- package/src/permissions/types.ts +0 -21
- package/src/platformProjects/index.ts +0 -3
- package/src/platformProjects/interface.ts +0 -27
- package/src/platformProjects/schemas.ts +0 -13
- package/src/platformProjects/types.ts +0 -22
- package/src/projects/index.ts +0 -4
- package/src/projects/interface.ts +0 -58
- package/src/projects/schemas.ts +0 -28
- package/src/projects/types.ts +0 -36
- package/src/shareLinks/index.ts +0 -5
- package/src/shareLinks/interface.ts +0 -35
- package/src/shareLinks/schemas.ts +0 -21
- package/src/shareLinks/types.ts +0 -34
- package/src/storage/assetClasses.ts +0 -134
- package/src/storage/cacheBust.ts +0 -26
- package/src/storage/image.ts +0 -82
- package/src/storage/index.ts +0 -12
- package/src/storage/interface.ts +0 -27
- package/src/testing/index.ts +0 -47
- package/src/testing/suites/authProviderSuite.ts +0 -207
- package/src/testing/suites/computeServerStoreSuite.ts +0 -195
- package/src/testing/suites/definitionStoreSuite.ts +0 -630
- package/src/testing/suites/emailLinkAuthSuite.ts +0 -83
- package/src/testing/suites/eventSinkSuite.ts +0 -379
- package/src/testing/suites/helpers.ts +0 -87
- package/src/testing/suites/inviteStoreSuite.ts +0 -143
- package/src/testing/suites/orgStoreSuite.ts +0 -429
- package/src/testing/suites/platformPermissionStoreSuite.ts +0 -196
- package/src/testing/suites/platformProjectGrantStoreSuite.ts +0 -166
- package/src/testing/suites/projectStoreSuite.ts +0 -381
- package/src/testing/suites/shareLinkStoreSuite.ts +0 -203
- package/src/testing/suites/solveMetricSinkSuite.ts +0 -135
- package/src/testing/suites/storageProviderSuite.ts +0 -196
- package/src/testing/suites/userProfileStoreSuite.ts +0 -134
- package/src/userProfile/index.ts +0 -3
- package/src/userProfile/interface.ts +0 -48
- package/src/userProfile/types.ts +0 -24
- package/src/utils/audit.ts +0 -46
- package/src/utils/index.ts +0 -2
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { RequestContext } from '../context.js';
|
|
2
|
-
import type { ListOptions, Page } from '../pagination.js';
|
|
3
|
-
import type { Organization, OrgMember } from './types.js';
|
|
4
|
-
import type { OrgRole, OrgPermission } from './schemas.js';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Organizations + org members. Tenancy boundary for everything else; reads and
|
|
8
|
-
* writes scope by `ctx.actingOrgId`.
|
|
9
|
-
*/
|
|
10
|
-
export interface IOrgStore {
|
|
11
|
-
// Organizations
|
|
12
|
-
listOrgs(ctx: RequestContext, opts?: ListOptions): Promise<Page<Organization>>;
|
|
13
|
-
getOrg(ctx: RequestContext, id: string): Promise<Organization | null>;
|
|
14
|
-
getOrgBySlug(ctx: RequestContext, slug: string): Promise<Organization | null>;
|
|
15
|
-
createOrg(ctx: RequestContext, org: Organization): Promise<void>;
|
|
16
|
-
updateOrg(
|
|
17
|
-
ctx: RequestContext,
|
|
18
|
-
id: string,
|
|
19
|
-
patch: Partial<Pick<Organization, 'name' | 'slug' | 'assets'>>
|
|
20
|
-
): Promise<void>;
|
|
21
|
-
/**
|
|
22
|
-
* Soft-delete the org. Cascades soft-delete to org members, projects in the
|
|
23
|
-
* org, and project members. Definitions and share links cascade through the
|
|
24
|
-
* project deletion. Invites and the org compute-server override are NOT
|
|
25
|
-
* cascaded — clean those up explicitly if needed.
|
|
26
|
-
*/
|
|
27
|
-
deleteOrg(ctx: RequestContext, id: string): Promise<void>;
|
|
28
|
-
|
|
29
|
-
// Org members
|
|
30
|
-
listOrgMembers(ctx: RequestContext, orgId: string, opts?: ListOptions): Promise<Page<OrgMember>>;
|
|
31
|
-
getOrgMember(ctx: RequestContext, orgId: string, userId: string): Promise<OrgMember | null>;
|
|
32
|
-
/**
|
|
33
|
-
* Find ONE org membership for the user. Used by the request-bootstrap
|
|
34
|
-
* path to resolve `actingOrgId` without N+1-ing over `listOrgs`.
|
|
35
|
-
*
|
|
36
|
-
* Returns `null` when the user has no live membership. When the user has
|
|
37
|
-
* multiple memberships (multi-tenant deployments), the choice between
|
|
38
|
-
* them is adapter-defined — single-tenant deployments have exactly one,
|
|
39
|
-
* and multi-tenant deployments will eventually use a URL prefix
|
|
40
|
-
* (`/o/{slug}/...`) to pick explicitly. Soft-deleted memberships are
|
|
41
|
-
* excluded.
|
|
42
|
-
*
|
|
43
|
-
* Both `org` and `member` are scoped by `ctx` (RLS-aware on Supabase).
|
|
44
|
-
*/
|
|
45
|
-
findUserMembership(
|
|
46
|
-
ctx: RequestContext,
|
|
47
|
-
userId: string
|
|
48
|
-
): Promise<{ org: Organization; member: OrgMember } | null>;
|
|
49
|
-
addOrgMember(ctx: RequestContext, member: OrgMember): Promise<void>;
|
|
50
|
-
updateOrgMemberRole(
|
|
51
|
-
ctx: RequestContext,
|
|
52
|
-
orgId: string,
|
|
53
|
-
userId: string,
|
|
54
|
-
role: OrgRole
|
|
55
|
-
): Promise<void>;
|
|
56
|
-
/**
|
|
57
|
-
* Replace `OrgPermission` set without changing role. Use this for
|
|
58
|
-
* permission edits — round-tripping through remove + add cascades the
|
|
59
|
-
* org-removal soft-delete into project memberships.
|
|
60
|
-
*/
|
|
61
|
-
updateOrgMemberPermissions(
|
|
62
|
-
ctx: RequestContext,
|
|
63
|
-
orgId: string,
|
|
64
|
-
userId: string,
|
|
65
|
-
permissions: readonly OrgPermission[]
|
|
66
|
-
): Promise<void>;
|
|
67
|
-
removeOrgMember(ctx: RequestContext, orgId: string, userId: string): Promise<void>;
|
|
68
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { OrgAssetKind } from './schemas.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Path-segment safety check. Blocks traversal, separators, NUL, and empty
|
|
5
|
-
* segments. Allowed alphabet `[A-Za-z0-9._-]+` covers GUIDs and sanitized
|
|
6
|
-
* refs. Called by every helper so a malicious `id` can't escape the
|
|
7
|
-
* `orgs/` prefix. Mirrors `definitionPaths`' guard.
|
|
8
|
-
*/
|
|
9
|
-
function assertSafeKey(value: string, label: string): void {
|
|
10
|
-
if (!value || !/^[A-Za-z0-9._-]+$/.test(value) || value === '.' || value === '..') {
|
|
11
|
-
throw new Error(`Unsafe ${label}: ${JSON.stringify(value)}`);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Storage keys for org-scoped assets. Every branding asset is stored as `.webp`
|
|
17
|
-
* (uploads are rasterized through the shared transcoder), so the path per kind
|
|
18
|
-
* is fixed — a re-upload overwrites in place via `IStorageProvider.put`'s
|
|
19
|
-
* upsert. The filename IS the kind, so the layout is generic: a new branding
|
|
20
|
-
* kind needs no new helper.
|
|
21
|
-
*
|
|
22
|
-
* Branding lives under `orgs/{id}/branding/` because it is a *public* asset
|
|
23
|
-
* class — anyone, incl. logged-out viewers, may read a company logo. The
|
|
24
|
-
* `branding/` segment keeps it distinguishable from future org-private blobs
|
|
25
|
-
* under `orgs/{id}/private/` (e.g. pricing sheets), which are members-only.
|
|
26
|
-
* The serving route and `getPublicUrl` decide public-vs-private by classifying
|
|
27
|
-
* these prefixes — see `classifyAssetPath` / `ASSET_CLASSES`.
|
|
28
|
-
*/
|
|
29
|
-
export const orgPaths = {
|
|
30
|
-
asset: (id: string, kind: OrgAssetKind) => {
|
|
31
|
-
assertSafeKey(id, 'orgId');
|
|
32
|
-
assertSafeKey(kind, 'assetKind');
|
|
33
|
-
return `orgs/${id}/branding/${kind}.webp`;
|
|
34
|
-
},
|
|
35
|
-
/** Members-only blob under the org's private tier. `name` includes the extension. */
|
|
36
|
-
privateAsset: (id: string, name: string) => {
|
|
37
|
-
assertSafeKey(id, 'orgId');
|
|
38
|
-
assertSafeKey(name, 'assetName');
|
|
39
|
-
return `orgs/${id}/private/${name}`;
|
|
40
|
-
},
|
|
41
|
-
prefix: (id: string) => {
|
|
42
|
-
assertSafeKey(id, 'orgId');
|
|
43
|
-
return `orgs/${id}/`;
|
|
44
|
-
}
|
|
45
|
-
} as const;
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
/** Lowercase alphanumeric with internal hyphens, 3–63 chars. */
|
|
4
|
-
export const SlugSchema = z
|
|
5
|
-
.string()
|
|
6
|
-
.min(3, 'Slug must be at least 3 characters')
|
|
7
|
-
.max(63, 'Slug must be at most 63 characters')
|
|
8
|
-
.regex(
|
|
9
|
-
/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/,
|
|
10
|
-
'Slug must be lowercase alphanumeric with hyphens (no leading/trailing/consecutive hyphens)'
|
|
11
|
-
);
|
|
12
|
-
|
|
13
|
-
export const OrgRoleSchema = z.enum(['owner', 'admin', 'member']);
|
|
14
|
-
export type OrgRole = z.infer<typeof OrgRoleSchema>;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Kinds of org-scoped branding asset. Adding a new kind is a single entry here
|
|
18
|
-
* — the storage path, upload route, store, and UI are all generic over it.
|
|
19
|
-
*/
|
|
20
|
-
export const OrgAssetKindSchema = z.enum(['logo', 'favicon']);
|
|
21
|
-
export type OrgAssetKind = z.infer<typeof OrgAssetKindSchema>;
|
|
22
|
-
|
|
23
|
-
export const ALL_ORG_ASSET_KINDS: readonly OrgAssetKind[] = OrgAssetKindSchema.options;
|
|
24
|
-
|
|
25
|
-
/** Org-scope permissions. A user may hold different sets in different orgs. */
|
|
26
|
-
export const OrgPermissionSchema = z.enum([
|
|
27
|
-
'manage_org_members',
|
|
28
|
-
'manage_org_compute',
|
|
29
|
-
'manage_definitions',
|
|
30
|
-
'manage_projects'
|
|
31
|
-
]);
|
|
32
|
-
export type OrgPermission = z.infer<typeof OrgPermissionSchema>;
|
|
33
|
-
|
|
34
|
-
export const ALL_ORG_PERMISSIONS: readonly OrgPermission[] = OrgPermissionSchema.options;
|
|
35
|
-
|
|
36
|
-
/** Governance perms — never grantable to `member`. */
|
|
37
|
-
export const OWNER_ADMIN_ONLY_PERMISSIONS: readonly OrgPermission[] = [
|
|
38
|
-
'manage_org_members',
|
|
39
|
-
'manage_org_compute'
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
export const MEMBER_ASSIGNABLE_PERMISSIONS: readonly OrgPermission[] = ALL_ORG_PERMISSIONS.filter(
|
|
43
|
-
(p) => !OWNER_ADMIN_ONLY_PERMISSIONS.includes(p)
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
/** Applied when adding a member without an explicit permissions list. */
|
|
47
|
-
export const DEFAULT_ORG_PERMISSIONS: Record<OrgRole, readonly OrgPermission[]> = {
|
|
48
|
-
owner: [...ALL_ORG_PERMISSIONS],
|
|
49
|
-
admin: [...ALL_ORG_PERMISSIONS],
|
|
50
|
-
member: []
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export const CreateOrgSchema = z.object({
|
|
54
|
-
name: z.string().min(1, 'Organization name is required').max(128),
|
|
55
|
-
slug: SlugSchema
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
export const UpdateOrgSchema = CreateOrgSchema.partial();
|
|
59
|
-
|
|
60
|
-
export type CreateOrgInput = z.infer<typeof CreateOrgSchema>;
|
|
61
|
-
export type UpdateOrgInput = z.infer<typeof UpdateOrgSchema>;
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type { OrgRole, OrgPermission, OrgAssetKind } from './schemas.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Public URLs of an org's branding assets, keyed by kind. Each value is what
|
|
5
|
-
* `IStorageProvider.getPublicUrl` returned for the stored blob; a kind is
|
|
6
|
-
* absent when unset. One small map so adding a new asset kind never touches
|
|
7
|
-
* the `Organization` shape, the migration, or the row mappers again.
|
|
8
|
-
*/
|
|
9
|
-
export type OrgAssets = Partial<Record<OrgAssetKind, string>>;
|
|
10
|
-
|
|
11
|
-
export interface Organization {
|
|
12
|
-
id: string;
|
|
13
|
-
name: string;
|
|
14
|
-
/** URL-safe unique identifier, e.g. "acme-corp". */
|
|
15
|
-
slug: string;
|
|
16
|
-
/** Transferable; distinct from `createdBy`. */
|
|
17
|
-
ownerId: string;
|
|
18
|
-
/** Immutable even on ownership transfer. */
|
|
19
|
-
createdBy: string;
|
|
20
|
-
updatedBy: string;
|
|
21
|
-
/** Branding assets (logo, favicon, …) by kind. Blobs live under `orgPaths.asset()`. */
|
|
22
|
-
assets?: OrgAssets;
|
|
23
|
-
createdAt: string;
|
|
24
|
-
updatedAt: string;
|
|
25
|
-
deletedAt?: string | null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface OrgMember {
|
|
29
|
-
orgId: string;
|
|
30
|
-
userId: string;
|
|
31
|
-
role: OrgRole;
|
|
32
|
-
/**
|
|
33
|
-
* `role` is the user-facing summary; `permissions` is what adapters check.
|
|
34
|
-
* Seeded from `DEFAULT_ORG_PERMISSIONS[role]` when not specified.
|
|
35
|
-
*/
|
|
36
|
-
permissions: OrgPermission[];
|
|
37
|
-
joinedAt: string;
|
|
38
|
-
updatedAt: string;
|
|
39
|
-
updatedBy: string;
|
|
40
|
-
deletedAt?: string | null;
|
|
41
|
-
}
|
package/src/pagination.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { DefinitionStatus } from './definitions/types.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Cursor-paginated list options. Adapters MUST provide sensible defaults
|
|
5
|
-
* (limit=50, orderBy=createdAt desc) and clamp limit to a safe upper bound.
|
|
6
|
-
*
|
|
7
|
-
* Cursors are opaque — adapters may use row ids, offsets, or keyset
|
|
8
|
-
* encodings. Callers must not inspect them.
|
|
9
|
-
*/
|
|
10
|
-
export interface ListOptions {
|
|
11
|
-
limit?: number;
|
|
12
|
-
cursor?: string;
|
|
13
|
-
orderBy?: 'createdAt' | 'updatedAt' | 'name';
|
|
14
|
-
/** Default: 'desc'. */
|
|
15
|
-
orderDir?: 'asc' | 'desc';
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Definition-specific filtering on top of `ListOptions`. */
|
|
19
|
-
export interface DefinitionListOptions extends Omit<ListOptions, 'orderBy'> {
|
|
20
|
-
orderBy?: 'createdAt' | 'updatedAt' | 'name' | 'solveCount';
|
|
21
|
-
/**
|
|
22
|
-
* Include records with `status='pending'`. Default false — consumers don't
|
|
23
|
-
* see half-written state unless they explicitly opt in.
|
|
24
|
-
*/
|
|
25
|
-
includePending?: boolean;
|
|
26
|
-
/**
|
|
27
|
-
* Include records with `status='archived'`. Default false — archived
|
|
28
|
-
* definitions are hidden from normal views; opt in for archive browsers.
|
|
29
|
-
*/
|
|
30
|
-
includeArchived?: boolean;
|
|
31
|
-
/**
|
|
32
|
-
* Filter to specific editorial statuses. Omit for the default view (all
|
|
33
|
-
* non-pending, non-archived). Pass `['published']` for the runner home.
|
|
34
|
-
* When set, `includePending` / `includeArchived` are ignored.
|
|
35
|
-
*/
|
|
36
|
-
statuses?: DefinitionStatus[];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface Page<T> {
|
|
40
|
-
items: T[];
|
|
41
|
-
nextCursor?: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export const DEFAULT_PAGE_LIMIT = 50;
|
|
45
|
-
export const MAX_PAGE_LIMIT = 200;
|
package/src/permissions/index.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import type { UserManagementResult } from '../auth/types.js';
|
|
2
|
-
import type { RequestContext } from '../context.js';
|
|
3
|
-
import type { PlatformPermission } from './types.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Per-user platform permissions. Lives at the data layer (not on
|
|
7
|
-
* `IAuthProvider`) so external IdPs don't need to model Selva-specific
|
|
8
|
-
* authorization.
|
|
9
|
-
*
|
|
10
|
-
* **The sole-`instance_admin` invariant is enforced here.** Any operation
|
|
11
|
-
* that would leave zero users holding `instance_admin` MUST return
|
|
12
|
-
* `'last_admin'`. Auth providers consult `countInstanceAdminsExcluding`
|
|
13
|
-
* before any destructive op so the invariant holds across edits, deletes,
|
|
14
|
-
* and disables.
|
|
15
|
-
*
|
|
16
|
-
* Reads are scoped to "self or admin"; writes restricted to `instance_admin`.
|
|
17
|
-
* Pass `SYSTEM_CONTEXT` for trusted server flows (bootstrap, hooks).
|
|
18
|
-
*/
|
|
19
|
-
export interface IPlatformPermissionStore {
|
|
20
|
-
/**
|
|
21
|
-
* Read permissions for a single user. Empty array for users with no
|
|
22
|
-
* admin grants (the common case). Non-admin callers may only read self.
|
|
23
|
-
*/
|
|
24
|
-
getFor(ctx: RequestContext, userId: string): Promise<PlatformPermission[]>;
|
|
25
|
-
|
|
26
|
-
/** Batch-read for the admin user list. Admin-only. */
|
|
27
|
-
getForBatch(
|
|
28
|
-
ctx: RequestContext,
|
|
29
|
-
userIds: readonly string[]
|
|
30
|
-
): Promise<Map<string, PlatformPermission[]>>;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Replace the user's permissions. Idempotent. Returns:
|
|
34
|
-
* - `ok` — succeeded
|
|
35
|
-
* - `not_found` — target user doesn't exist
|
|
36
|
-
* - `last_admin` — refused; would leave zero `instance_admin` users
|
|
37
|
-
*
|
|
38
|
-
* `not_supported` is intentionally absent — every deployment must provide
|
|
39
|
-
* a permission store.
|
|
40
|
-
*/
|
|
41
|
-
set(
|
|
42
|
-
ctx: RequestContext,
|
|
43
|
-
userId: string,
|
|
44
|
-
permissions: readonly PlatformPermission[]
|
|
45
|
-
): Promise<UserManagementResult>;
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* True if at least one *enabled* user holds `instance_admin`. Used by
|
|
49
|
-
* first-run detection and the destructive-op invariant check. Adapters
|
|
50
|
-
* MUST exclude disabled users (over-count is safer than under-count).
|
|
51
|
-
*/
|
|
52
|
-
hasInstanceAdmin(ctx: RequestContext): Promise<boolean>;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Count *other* users holding `instance_admin`. `deleteUser` / `disableUser`
|
|
56
|
-
* call this BEFORE the destructive op — `count === 0` ⇒ `'last_admin'`.
|
|
57
|
-
* MUST exclude disabled users.
|
|
58
|
-
*/
|
|
59
|
-
countInstanceAdminsExcluding(ctx: RequestContext, excludeUserId: string): Promise<number>;
|
|
60
|
-
}
|
package/src/permissions/types.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Platform-scope permissions. Reserved for Selva-staff and instance operators;
|
|
5
|
-
* regular users hold an empty array.
|
|
6
|
-
*
|
|
7
|
-
* - `instance_admin` — superuser; implies every other permission.
|
|
8
|
-
* - `manage_compute` — configure the instance-wide Rhino.Compute pool.
|
|
9
|
-
* - `manage_instance_users` — disable/enable any user on the instance.
|
|
10
|
-
* - `manage_updates` — run system updates.
|
|
11
|
-
*/
|
|
12
|
-
export const PlatformPermissionSchema = z.enum([
|
|
13
|
-
'instance_admin',
|
|
14
|
-
'manage_compute',
|
|
15
|
-
'manage_instance_users',
|
|
16
|
-
'manage_updates'
|
|
17
|
-
]);
|
|
18
|
-
export type PlatformPermission = z.infer<typeof PlatformPermissionSchema>;
|
|
19
|
-
|
|
20
|
-
export const ALL_PLATFORM_PERMISSIONS: readonly PlatformPermission[] =
|
|
21
|
-
PlatformPermissionSchema.options;
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { RequestContext } from '../context.js';
|
|
2
|
-
import type { PlatformProjectGrant } from './types.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Grants for `platform`-visibility projects. Enables `instance_admin` to
|
|
6
|
-
* grant orgs or individual users view-only or view+solve access without
|
|
7
|
-
* creating project membership rows.
|
|
8
|
-
*
|
|
9
|
-
* All deletes are hard-deletes — grants have no downstream references and
|
|
10
|
-
* revocation is immediate.
|
|
11
|
-
*/
|
|
12
|
-
export interface IPlatformProjectGrantStore {
|
|
13
|
-
listByProject(ctx: RequestContext, projectId: string): Promise<PlatformProjectGrant[]>;
|
|
14
|
-
create(ctx: RequestContext, grant: PlatformProjectGrant): Promise<void>;
|
|
15
|
-
delete(ctx: RequestContext, id: string): Promise<void>;
|
|
16
|
-
/**
|
|
17
|
-
* Cascade hook: drop every grant for a project. Called by `deleteProject`.
|
|
18
|
-
* No-op when none exist.
|
|
19
|
-
*/
|
|
20
|
-
deleteByProject(ctx: RequestContext, projectId: string): Promise<void>;
|
|
21
|
-
/**
|
|
22
|
-
* Cascade hook: drop every org grant whose `granteeId === orgId`. Called by
|
|
23
|
-
* `deleteOrg`. User grants are identity-scoped and remain. No-op when none
|
|
24
|
-
* exist.
|
|
25
|
-
*/
|
|
26
|
-
deleteByGranteeOrg(ctx: RequestContext, orgId: string): Promise<void>;
|
|
27
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
export const PlatformProjectGranteeTypeSchema = z.enum(['org', 'user']);
|
|
4
|
-
|
|
5
|
-
export const PlatformProjectGrantSchema = z.object({
|
|
6
|
-
id: z.string(),
|
|
7
|
-
projectId: z.string(),
|
|
8
|
-
granteeType: PlatformProjectGranteeTypeSchema,
|
|
9
|
-
granteeId: z.string(),
|
|
10
|
-
canSolve: z.boolean(),
|
|
11
|
-
createdBy: z.string(),
|
|
12
|
-
createdAt: z.string()
|
|
13
|
-
});
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export type PlatformProjectGranteeType = 'org' | 'user';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Grants view-only or view+solve access to a `platform`-visibility project
|
|
5
|
-
* for an org or an individual user, without creating project membership rows.
|
|
6
|
-
*
|
|
7
|
-
* - `granteeType: 'org'` → applies to all members of that org
|
|
8
|
-
* - `granteeType: 'user'` → applies to that specific user regardless of org
|
|
9
|
-
* - `canSolve: false` → schema fetch / view only; submit-solve is blocked
|
|
10
|
-
* - `canSolve: true` → view + solve
|
|
11
|
-
*
|
|
12
|
-
* Grants are hard-deleted on revocation — no soft-delete.
|
|
13
|
-
*/
|
|
14
|
-
export interface PlatformProjectGrant {
|
|
15
|
-
id: string;
|
|
16
|
-
projectId: string;
|
|
17
|
-
granteeType: PlatformProjectGranteeType;
|
|
18
|
-
granteeId: string;
|
|
19
|
-
canSolve: boolean;
|
|
20
|
-
createdBy: string;
|
|
21
|
-
createdAt: string;
|
|
22
|
-
}
|
package/src/projects/index.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
export type { Project, ProjectMember } from './types.js';
|
|
2
|
-
export type { IProjectStore } from './interface.js';
|
|
3
|
-
export type { ProjectVisibility, ProjectRole } from './schemas.js';
|
|
4
|
-
export { ProjectVisibilitySchema, ProjectRoleSchema, validateProjectFlags } from './schemas.js';
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import type { RequestContext } from '../context.js';
|
|
2
|
-
import type { ListOptions, Page } from '../pagination.js';
|
|
3
|
-
import type { Project, ProjectMember } from './types.js';
|
|
4
|
-
import type { ProjectRole } from './schemas.js';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Projects + project members. Soft-delete cascades to members and definitions;
|
|
8
|
-
* see also the cross-store cascade hook on `IPlatformProjectGrantStore`.
|
|
9
|
-
*/
|
|
10
|
-
export interface IProjectStore {
|
|
11
|
-
// Projects
|
|
12
|
-
listProjects(ctx: RequestContext, orgId: string, opts?: ListOptions): Promise<Page<Project>>;
|
|
13
|
-
getProject(ctx: RequestContext, id: string): Promise<Project | null>;
|
|
14
|
-
getProjectBySlug(ctx: RequestContext, orgId: string, slug: string): Promise<Project | null>;
|
|
15
|
-
createProject(ctx: RequestContext, project: Project): Promise<void>;
|
|
16
|
-
updateProject(
|
|
17
|
-
ctx: RequestContext,
|
|
18
|
-
id: string,
|
|
19
|
-
patch: Partial<
|
|
20
|
-
Pick<Project, 'name' | 'slug' | 'description' | 'visibility' | 'autoJoinOnUpload'>
|
|
21
|
-
>
|
|
22
|
-
): Promise<void>;
|
|
23
|
-
/**
|
|
24
|
-
* Soft-delete the project. Cascades to project members and definitions.
|
|
25
|
-
* Definition versions and share links cascade through the definition delete.
|
|
26
|
-
*/
|
|
27
|
-
deleteProject(ctx: RequestContext, id: string): Promise<void>;
|
|
28
|
-
/**
|
|
29
|
-
* Reactivate a previously soft-deleted project identified by its org + slug.
|
|
30
|
-
* Clears `deleted_at` and reactivates the owner's `project_members` row.
|
|
31
|
-
* Returns the live project, or `null` if no tombstone with that slug exists.
|
|
32
|
-
*
|
|
33
|
-
* Use this when `createProject` fails with a duplicate-key error on a slug
|
|
34
|
-
* that was soft-deleted — the unconditional unique constraint blocks creation
|
|
35
|
-
* even though `getProjectBySlug` returns null for tombstones.
|
|
36
|
-
*/
|
|
37
|
-
reactivateProject(ctx: RequestContext, orgId: string, slug: string): Promise<Project | null>;
|
|
38
|
-
|
|
39
|
-
// Project members
|
|
40
|
-
listProjectMembers(
|
|
41
|
-
ctx: RequestContext,
|
|
42
|
-
projectId: string,
|
|
43
|
-
opts?: ListOptions
|
|
44
|
-
): Promise<Page<ProjectMember>>;
|
|
45
|
-
getProjectMember(
|
|
46
|
-
ctx: RequestContext,
|
|
47
|
-
projectId: string,
|
|
48
|
-
userId: string
|
|
49
|
-
): Promise<ProjectMember | null>;
|
|
50
|
-
addProjectMember(ctx: RequestContext, member: ProjectMember): Promise<void>;
|
|
51
|
-
updateProjectMemberRole(
|
|
52
|
-
ctx: RequestContext,
|
|
53
|
-
projectId: string,
|
|
54
|
-
userId: string,
|
|
55
|
-
role: ProjectRole
|
|
56
|
-
): Promise<void>;
|
|
57
|
-
removeProjectMember(ctx: RequestContext, projectId: string, userId: string): Promise<void>;
|
|
58
|
-
}
|
package/src/projects/schemas.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
export const ProjectVisibilitySchema = z.enum(['public', 'org', 'private', 'platform']);
|
|
4
|
-
export type ProjectVisibility = z.infer<typeof ProjectVisibilitySchema>;
|
|
5
|
-
|
|
6
|
-
export const ProjectRoleSchema = z.enum(['owner', 'editor', 'viewer']);
|
|
7
|
-
export type ProjectRole = z.infer<typeof ProjectRoleSchema>;
|
|
8
|
-
|
|
9
|
-
/** Cross-field check for PATCH flows. Call on the merged post-patch shape. */
|
|
10
|
-
export function validateProjectFlags(merged: {
|
|
11
|
-
visibility: ProjectVisibility;
|
|
12
|
-
autoJoinOnUpload?: boolean;
|
|
13
|
-
}): { path: string; message: string }[] {
|
|
14
|
-
const issues: { path: string; message: string }[] = [];
|
|
15
|
-
if (merged.autoJoinOnUpload && merged.visibility !== 'public') {
|
|
16
|
-
issues.push({
|
|
17
|
-
path: 'autoJoinOnUpload',
|
|
18
|
-
message: 'autoJoinOnUpload requires visibility=public'
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
if (merged.visibility === 'platform' && merged.autoJoinOnUpload) {
|
|
22
|
-
issues.push({
|
|
23
|
-
path: 'autoJoinOnUpload',
|
|
24
|
-
message: 'autoJoinOnUpload is not allowed on platform projects'
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
return issues;
|
|
28
|
-
}
|
package/src/projects/types.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import type { ProjectRole, ProjectVisibility } from './schemas.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Visibility:
|
|
5
|
-
* - `public` — any authenticated user can view/solve (cross-org gated by `ALLOW_CROSS_ORG_PUBLIC`)
|
|
6
|
-
* - `org` — any member of the parent organization
|
|
7
|
-
* - `private` — only listed `ProjectMember`s
|
|
8
|
-
* - `platform` — instance-admin-managed; access via `IPlatformProjectGrantStore`
|
|
9
|
-
*/
|
|
10
|
-
export interface Project {
|
|
11
|
-
id: string;
|
|
12
|
-
orgId: string;
|
|
13
|
-
name: string;
|
|
14
|
-
slug: string;
|
|
15
|
-
description?: string;
|
|
16
|
-
visibility: ProjectVisibility;
|
|
17
|
-
/** Transferable; distinct from `createdBy`. */
|
|
18
|
-
ownerId: string;
|
|
19
|
-
createdBy: string;
|
|
20
|
-
updatedBy: string;
|
|
21
|
-
/** Commons mode — only valid when `visibility='public'`. */
|
|
22
|
-
autoJoinOnUpload: boolean;
|
|
23
|
-
createdAt: string;
|
|
24
|
-
updatedAt: string;
|
|
25
|
-
deletedAt?: string | null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface ProjectMember {
|
|
29
|
-
projectId: string;
|
|
30
|
-
userId: string;
|
|
31
|
-
role: ProjectRole;
|
|
32
|
-
joinedAt: string;
|
|
33
|
-
updatedAt: string;
|
|
34
|
-
updatedBy: string;
|
|
35
|
-
deletedAt?: string | null;
|
|
36
|
-
}
|
package/src/shareLinks/index.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export type { ShareLink } from './types.js';
|
|
2
|
-
export type { IShareLinkStore } from './interface.js';
|
|
3
|
-
export { DEFAULT_SHARE_LINK_MAX_SOLVES } from './types.js';
|
|
4
|
-
export type { CreateShareLinkInput } from './schemas.js';
|
|
5
|
-
export { CreateShareLinkInputSchema } from './schemas.js';
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { RequestContext } from '../context.js';
|
|
2
|
-
import type { ListOptions, Page } from '../pagination.js';
|
|
3
|
-
import type { ShareLink } from './types.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Per-definition tokens granting unauthenticated access to one
|
|
7
|
-
* (definitionId, channel). The store sees only the HMAC hash; the raw token
|
|
8
|
-
* is generated and shown to the minter at the route layer.
|
|
9
|
-
*
|
|
10
|
-
* Reads filter `revokedAt IS NULL` defensively.
|
|
11
|
-
*/
|
|
12
|
-
export interface IShareLinkStore {
|
|
13
|
-
/** Insert a new link. Caller has already hashed the raw token. */
|
|
14
|
-
create(ctx: RequestContext, link: ShareLink): Promise<void>;
|
|
15
|
-
/** Newest first by `createdAt`. Excludes revoked links. */
|
|
16
|
-
listByDefinition(
|
|
17
|
-
ctx: RequestContext,
|
|
18
|
-
definitionId: string,
|
|
19
|
-
opts?: ListOptions
|
|
20
|
-
): Promise<Page<ShareLink>>;
|
|
21
|
-
getById(ctx: RequestContext, id: string): Promise<ShareLink | null>;
|
|
22
|
-
/**
|
|
23
|
-
* Lookup by HMAC hash. Returns null when the link doesn't exist OR is
|
|
24
|
-
* revoked OR its parent definition is soft-deleted.
|
|
25
|
-
*/
|
|
26
|
-
getByTokenHash(ctx: RequestContext, tokenHash: string): Promise<ShareLink | null>;
|
|
27
|
-
/** Soft-delete (set `revokedAt`). Idempotent. */
|
|
28
|
-
revoke(ctx: RequestContext, id: string): Promise<void>;
|
|
29
|
-
/**
|
|
30
|
-
* Atomic check-and-increment. Returns the new `solveCount`, or null when
|
|
31
|
-
* the cap was reached. MUST be a single statement — read-then-write races
|
|
32
|
-
* under load. `null` maxSolves means uncapped.
|
|
33
|
-
*/
|
|
34
|
-
tryIncrementSolveCount(ctx: RequestContext, id: string): Promise<number | null>;
|
|
35
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { DefinitionChannelSchema } from '../definitions/schemas.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* HTTP body for `POST /api/definitions/[guid]/share-links`. The route returns
|
|
6
|
-
* the raw token exactly once.
|
|
7
|
-
*/
|
|
8
|
-
export const CreateShareLinkInputSchema = z.object({
|
|
9
|
-
channel: DefinitionChannelSchema.default('live'),
|
|
10
|
-
name: z.string().max(128).optional(),
|
|
11
|
-
allowSolve: z.boolean().default(true),
|
|
12
|
-
/**
|
|
13
|
-
* Null explicitly removes the cap. Omitted = apply default — null is the
|
|
14
|
-
* minter's deliberate "uncap" choice.
|
|
15
|
-
*/
|
|
16
|
-
maxSolves: z.number().int().min(1).nullable().optional(),
|
|
17
|
-
/** Optional ISO timestamp. Resolution compares to `now()`. */
|
|
18
|
-
expiresAt: z.iso.datetime().nullable().optional()
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
export type CreateShareLinkInput = z.infer<typeof CreateShareLinkInputSchema>;
|
package/src/shareLinks/types.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { DefinitionChannel } from '../definitions/types.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Per-definition token granting access to one (definitionId, channel)
|
|
5
|
-
* without an account. The raw token is HMAC-hashed at rest; `tokenHash` is
|
|
6
|
-
* the stored value. The plaintext token is shown to the minter exactly once.
|
|
7
|
-
*/
|
|
8
|
-
export interface ShareLink {
|
|
9
|
-
id: string;
|
|
10
|
-
definitionId: string;
|
|
11
|
-
channel: DefinitionChannel;
|
|
12
|
-
/** HMAC of the raw token. Lookup hashes the supplied token, then matches. */
|
|
13
|
-
tokenHash: string;
|
|
14
|
-
/** Optional UX label, e.g. "Demo for Acme". */
|
|
15
|
-
name?: string;
|
|
16
|
-
createdBy: string;
|
|
17
|
-
createdAt: string;
|
|
18
|
-
/** Null = never expires. */
|
|
19
|
-
expiresAt?: string | null;
|
|
20
|
-
/** Non-null = revoked. Resolution checks IS NULL. */
|
|
21
|
-
revokedAt?: string | null;
|
|
22
|
-
/** false = view/schema only; true = solve allowed. */
|
|
23
|
-
allowSolve: boolean;
|
|
24
|
-
/** Per-link cap. Null = unlimited. */
|
|
25
|
-
maxSolves?: number | null;
|
|
26
|
-
solveCount: number;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Default cap applied when the minter doesn't specify one. Tokens are
|
|
31
|
-
* leak-prone by design (visible in iframes), so a default bounds the
|
|
32
|
-
* worst-case denial-of-wallet damage.
|
|
33
|
-
*/
|
|
34
|
-
export const DEFAULT_SHARE_LINK_MAX_SOLVES = 1000;
|