Skip to content
Merged
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
48 changes: 48 additions & 0 deletions packages/node-type-registry/src/authz/authz-app-membership.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { NodeTypeDefinition } from '../types';

export const AuthzAppMembership: NodeTypeDefinition = {
name: 'AuthzAppMembership',
slug: 'authz_app_membership_check',
category: 'authz',
display_name: 'App Membership Check',
description:
'App-level membership check (membership_type=1). Verifies the user has app membership (optionally with specific permission) without binding to any entity from the row. Uses EXISTS subquery against SPRT table. Replaces AuthzMembership for clarity.',
parameter_schema: {
type: 'object',
properties: {
membership_type: {
type: ['integer', 'string'],
description:
'Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)',
},
entity_type: {
type: 'string',
description:
"Entity type prefix (e.g. 'channel', 'department'). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability.",
},
permission: {
type: 'string',
description:
'Single permission name to check (resolved to bitstring mask)',
},
permissions: {
type: 'array',
items: {
type: 'string',
},
description:
'Multiple permission names to check (ORed together into mask)',
},
is_admin: {
type: 'boolean',
description: 'If true, require is_admin flag',
},
is_owner: {
type: 'boolean',
description: 'If true, require is_owner flag',
},
},
required: [],
},
tags: ['membership', 'authz'],
};
49 changes: 0 additions & 49 deletions packages/node-type-registry/src/authz/authz-membership-check.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/node-type-registry/src/authz/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
export { AuthzAllowAll } from './authz-allow-all';
export { AuthzAppMembership } from './authz-app-membership';
export { AuthzComposite } from './authz-composite';
export { AuthzDenyAll } from './authz-deny-all';
export { AuthzDirectOwner } from './authz-direct-owner';
export { AuthzDirectOwnerAny } from './authz-direct-owner-any';
export { AuthzEntityMembership } from './authz-entity-membership';
export { AuthzMemberList } from './authz-member-list';
export { AuthzMembership } from './authz-membership-check';
export { AuthzNotReadOnly } from './authz-not-read-only';
export { AuthzOrgHierarchy } from './authz-org-hierarchy';
export { AuthzPeerOwnership } from './authz-peer-ownership';
Expand Down
66 changes: 46 additions & 20 deletions packages/node-type-registry/src/blueprint-types.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ export interface DataEntityMembershipParams {
/* If true, adds a foreign key constraint from entity_id to the users table */
include_user_fk?: boolean;
}
/** Gates a table behind a feature flag backed by the cap tables. Attaches a BEFORE INSERT trigger that checks whether the named feature cap value is > 0. Features are modeled as caps with max=0 (disabled) or max=1 (enabled) in limit_caps / limit_caps_defaults tables. Resolution: COALESCE(per-entity cap, scope default, 0). */
export interface DataFeatureFlagParams {
/* Cap name representing this feature (must match a limit_caps_defaults entry with max=0 or max=1) */
feature_name: string;
/* Feature scope: "app" (membership_type=1, app-level caps) or "org" (membership_type=2, per-entity caps) */
scope?: 'app' | 'org';
/* Column on the target table that holds the entity id for per-entity cap lookups (only used for org scope) */
entity_field?: string;
}
/** BEFORE INSERT trigger that forces a field to the value of jwt_public.current_user_id(). Prevents clients from spoofing the actor/uploader identity. The field value is always overwritten regardless of what the client provides. */
export interface DataForceCurrentUserParams {
/* Name of the field to force to current_user_id() */
Expand Down Expand Up @@ -157,6 +166,17 @@ export interface DataJobTriggerParams {
/* Maximum retry attempts for the job */
max_attempts?: number;
}
/** Declaratively attaches limit-tracking triggers to a table. On INSERT the named limit is incremented; on DELETE it is decremented. Requires a provisioned limits_module for the target scope. */
export interface DataLimitCounterParams {
/* Name of the limit to track (must match a default_limits entry, e.g. "projects", "members") */
limit_name: string;
/* Limit scope: "app" (membership_type=1, user-level) or "org" (membership_type=2, entity-level) */
scope?: 'app' | 'org';
/* Column on the target table that holds the actor or entity id used for limit lookup */
actor_field?: string;
/* Which DML events to attach triggers for */
events?: ('INSERT' | 'DELETE' | 'UPDATE')[];
}
/** Adds a JSONB column with optional GIN index for containment queries (@>, ?, ?|, ?&). Standard pattern for semi-structured metadata. */
export interface DataJsonbParams {
/* Column name for the JSONB field */
Expand Down Expand Up @@ -432,6 +452,21 @@ export interface SearchVectorParams {
;
/** Allows all access. Generates TRUE expression. */
export type AuthzAllowAllParams = {};
/** App-level membership check (membership_type=1). Verifies the user has app membership (optionally with specific permission) without binding to any entity from the row. Uses EXISTS subquery against SPRT table. Replaces AuthzMembership for clarity. */
export interface AuthzAppMembershipParams {
/* Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module) */
membership_type?: number | string;
/* Entity type prefix (e.g. 'channel', 'department'). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability. */
entity_type?: string;
/* Single permission name to check (resolved to bitstring mask) */
permission?: string;
/* Multiple permission names to check (ORed together into mask) */
permissions?: string[];
/* If true, require is_admin flag */
is_admin?: boolean;
/* If true, require is_owner flag */
is_owner?: boolean;
}
/** Composite authorization policy that combines multiple authorization nodes using boolean logic (AND/OR). The data field contains a JSONB AST with nested authorization nodes. */
export interface AuthzCompositeParams {
/* Boolean expression combining multiple authorization nodes */
Expand Down Expand Up @@ -478,21 +513,6 @@ export interface AuthzMemberListParams {
/* Column name containing the array of user IDs */
array_field: string;
}
/** Membership check that verifies the user has membership (optionally with specific permission) without binding to any entity from the row. Uses EXISTS subquery against SPRT table. */
export interface AuthzMembershipParams {
/* Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module) */
membership_type?: number | string;
/* Entity type prefix (e.g. 'channel', 'department'). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability. */
entity_type?: string;
/* Single permission name to check (resolved to bitstring mask) */
permission?: string;
/* Multiple permission names to check (ORed together into mask) */
permissions?: string[];
/* If true, require is_admin flag */
is_admin?: boolean;
/* If true, require is_owner flag */
is_owner?: boolean;
}
/** Restrictive policy that blocks read-only members from mutations. Checks actor_id + is_read_only IS NOT TRUE on the SPRT. Designed to run as a restrictive counterpart after a permissive AuthzEntityMembership policy has already verified membership. */
export interface AuthzNotReadOnlyParams {
/* Column name referencing the entity (e.g., entity_id, org_id) */
Expand Down Expand Up @@ -809,7 +829,7 @@ export interface BlueprintField {
/** An RLS policy entry for a blueprint table. Uses $type to match the blueprint JSON convention. */
export interface BlueprintPolicy {
/** Authz* policy type name (e.g., "AuthzDirectOwner", "AuthzAllowAll"). */
$type: 'AuthzAllowAll' | 'AuthzComposite' | 'AuthzDenyAll' | 'AuthzDirectOwner' | 'AuthzDirectOwnerAny' | 'AuthzEntityMembership' | 'AuthzMemberList' | 'AuthzMembership' | 'AuthzNotReadOnly' | 'AuthzOrgHierarchy' | 'AuthzPeerOwnership' | 'AuthzPublishable' | 'AuthzRelatedEntityMembership' | 'AuthzRelatedMemberList' | 'AuthzRelatedPeerOwnership' | 'AuthzTemporal';
$type: 'AuthzAllowAll' | 'AuthzAppMembership' | 'AuthzComposite' | 'AuthzDenyAll' | 'AuthzDirectOwner' | 'AuthzDirectOwnerAny' | 'AuthzEntityMembership' | 'AuthzMemberList' | 'AuthzNotReadOnly' | 'AuthzOrgHierarchy' | 'AuthzPeerOwnership' | 'AuthzPublishable' | 'AuthzRelatedEntityMembership' | 'AuthzRelatedMemberList' | 'AuthzRelatedPeerOwnership' | 'AuthzTemporal';
/** Privileges this policy applies to (e.g., ["select"], ["insert", "update", "delete"]). */
privileges?: string[];
/** Whether this policy is permissive (true) or restrictive (false). Defaults to true. */
Expand Down Expand Up @@ -993,11 +1013,14 @@ export interface BlueprintEntityType {
*/
;
/** String shorthand -- just the node type name. */
export type BlueprintNodeShorthand = 'AuthzAllowAll' | 'AuthzComposite' | 'AuthzDenyAll' | 'AuthzDirectOwner' | 'AuthzDirectOwnerAny' | 'AuthzEntityMembership' | 'AuthzMemberList' | 'AuthzMembership' | 'AuthzNotReadOnly' | 'AuthzOrgHierarchy' | 'AuthzPeerOwnership' | 'AuthzPublishable' | 'AuthzRelatedEntityMembership' | 'AuthzRelatedMemberList' | 'AuthzRelatedPeerOwnership' | 'AuthzTemporal' | 'DataCompositeField' | 'DataDirectOwner' | 'DataEntityMembership' | 'DataForceCurrentUser' | 'DataId' | 'DataImageEmbedding' | 'DataImmutableFields' | 'DataInflection' | 'DataInheritFromParent' | 'DataJobTrigger' | 'DataJsonb' | 'DataOwnedFields' | 'DataOwnershipInEntity' | 'DataPeoplestamps' | 'DataPublishable' | 'DataSlug' | 'DataSoftDelete' | 'DataStatusField' | 'DataTags' | 'DataTimestamps' | 'SearchBm25' | 'SearchFullText' | 'SearchSpatial' | 'SearchSpatialAggregate' | 'SearchTrgm' | 'SearchUnified' | 'SearchVector' | 'TableOrganizationSettings' | 'TableUserProfiles' | 'TableUserSettings';
export type BlueprintNodeShorthand = 'AuthzAllowAll' | 'AuthzAppMembership' | 'AuthzComposite' | 'AuthzDenyAll' | 'AuthzDirectOwner' | 'AuthzDirectOwnerAny' | 'AuthzEntityMembership' | 'AuthzMemberList' | 'AuthzNotReadOnly' | 'AuthzOrgHierarchy' | 'AuthzPeerOwnership' | 'AuthzPublishable' | 'AuthzRelatedEntityMembership' | 'AuthzRelatedMemberList' | 'AuthzRelatedPeerOwnership' | 'AuthzTemporal' | 'DataCompositeField' | 'DataDirectOwner' | 'DataEntityMembership' | 'DataFeatureFlag' | 'DataForceCurrentUser' | 'DataId' | 'DataImageEmbedding' | 'DataImmutableFields' | 'DataInflection' | 'DataInheritFromParent' | 'DataJobTrigger' | 'DataLimitCounter' | 'DataJsonb' | 'DataOwnedFields' | 'DataOwnershipInEntity' | 'DataPeoplestamps' | 'DataPublishable' | 'DataSlug' | 'DataSoftDelete' | 'DataStatusField' | 'DataTags' | 'DataTimestamps' | 'SearchBm25' | 'SearchFullText' | 'SearchSpatial' | 'SearchSpatialAggregate' | 'SearchTrgm' | 'SearchUnified' | 'SearchVector' | 'TableOrganizationSettings' | 'TableUserProfiles' | 'TableUserSettings';
/** Object form -- { $type, data } with typed parameters. */
export type BlueprintNodeObject = {
$type: 'AuthzAllowAll';
data?: Record<string, never>;
} | {
$type: 'AuthzAppMembership';
data: AuthzAppMembershipParams;
} | {
$type: 'AuthzComposite';
data: AuthzCompositeParams;
Expand All @@ -1016,9 +1039,6 @@ export type BlueprintNodeObject = {
} | {
$type: 'AuthzMemberList';
data: AuthzMemberListParams;
} | {
$type: 'AuthzMembership';
data: AuthzMembershipParams;
} | {
$type: 'AuthzNotReadOnly';
data: AuthzNotReadOnlyParams;
Expand Down Expand Up @@ -1052,6 +1072,9 @@ export type BlueprintNodeObject = {
} | {
$type: 'DataEntityMembership';
data: DataEntityMembershipParams;
} | {
$type: 'DataFeatureFlag';
data: DataFeatureFlagParams;
} | {
$type: 'DataForceCurrentUser';
data: DataForceCurrentUserParams;
Expand All @@ -1073,6 +1096,9 @@ export type BlueprintNodeObject = {
} | {
$type: 'DataJobTrigger';
data: DataJobTriggerParams;
} | {
$type: 'DataLimitCounter';
data: DataLimitCounterParams;
} | {
$type: 'DataJsonb';
data: DataJsonbParams;
Expand Down
36 changes: 36 additions & 0 deletions packages/node-type-registry/src/data/data-feature-flag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { NodeTypeDefinition } from '../types';

export const DataFeatureFlag: NodeTypeDefinition = {
name: 'DataFeatureFlag',
slug: 'data_feature_flag',
category: 'data',
display_name: 'Feature Flag',
description:
'Gates a table behind a feature flag backed by the cap tables. Attaches a BEFORE INSERT trigger that checks whether the named feature cap value is > 0. Features are modeled as caps with max=0 (disabled) or max=1 (enabled) in limit_caps / limit_caps_defaults tables. Resolution: COALESCE(per-entity cap, scope default, 0).',
parameter_schema: {
type: 'object',
properties: {
feature_name: {
type: 'string',
description:
'Cap name representing this feature (must match a limit_caps_defaults entry with max=0 or max=1)',
},
scope: {
type: 'string',
enum: ['app', 'org'],
description:
'Feature scope: "app" (membership_type=1, app-level caps) or "org" (membership_type=2, per-entity caps)',
default: 'app',
},
entity_field: {
type: 'string',
format: 'column-ref',
description:
'Column on the target table that holds the entity id for per-entity cap lookups (only used for org scope)',
default: 'entity_id',
},
},
required: ['feature_name'],
},
tags: ['limits', 'triggers', 'feature-flags', 'billing', 'caps'],
};
46 changes: 46 additions & 0 deletions packages/node-type-registry/src/data/data-limit-counter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { NodeTypeDefinition } from '../types';

export const DataLimitCounter: NodeTypeDefinition = {
name: 'DataLimitCounter',
slug: 'data_limit_counter',
category: 'data',
display_name: 'Limit Counter',
description:
'Declaratively attaches limit-tracking triggers to a table. On INSERT the named limit is incremented; on DELETE it is decremented. Requires a provisioned limits_module for the target scope.',
parameter_schema: {
type: 'object',
properties: {
limit_name: {
type: 'string',
description:
'Name of the limit to track (must match a default_limits entry, e.g. "projects", "members")',
},
scope: {
type: 'string',
enum: ['app', 'org'],
description:
'Limit scope: "app" (membership_type=1, user-level) or "org" (membership_type=2, entity-level)',
default: 'app',
},
actor_field: {
type: 'string',
format: 'column-ref',
description:
'Column on the target table that holds the actor or entity id used for limit lookup',
default: 'owner_id',
},
events: {
type: 'array',
items: {
type: 'string',
enum: ['INSERT', 'DELETE', 'UPDATE'],
},
description:
'Which DML events to attach triggers for',
default: ['INSERT', 'DELETE'],
},
},
required: ['limit_name'],
},
tags: ['limits', 'triggers', 'billing'],
};
2 changes: 2 additions & 0 deletions packages/node-type-registry/src/data/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
export { DataCompositeField } from './data-composite-field';
export { DataDirectOwner } from './data-direct-owner';
export { DataEntityMembership } from './data-entity-membership';
export { DataFeatureFlag } from './data-feature-flag';
export { DataForceCurrentUser } from './data-force-current-user';
export { DataId } from './data-id';
export { DataImageEmbedding } from './data-image-embedding';
export { DataImmutableFields } from './data-immutable-fields';
export { DataInflection } from './data-inflection';
export { DataInheritFromParent } from './data-inherit-from-parent';
export { DataJobTrigger } from './data-job-trigger';
export { DataLimitCounter } from './data-limit-counter';
export { DataJsonb } from './data-jsonb';
export { DataOwnedFields } from './data-owned-fields';
export { DataOwnershipInEntity } from './data-ownership-in-entity';
Expand Down
Loading