@burdenoff/be-sdk 2026.528.1 → 2026.528.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/dist/{chunk-7BMKOD7N.js → chunk-VDDOGIWY.js} +4 -2
- package/dist/chunk-VDDOGIWY.js.map +1 -0
- package/dist/chunk-XUU5IBVU.js +449 -0
- package/dist/chunk-XUU5IBVU.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/resource-caps/index.js +1 -1
- package/dist/server/index.d.ts +43 -2
- package/dist/server/index.js +23 -8
- package/dist/server/index.js.map +1 -1
- package/dist/workspace-guard-3TKZULCO.js +5 -0
- package/dist/{workspace-guard-524GBJQ6.js.map → workspace-guard-3TKZULCO.js.map} +1 -1
- package/package.json +1 -1
- package/dist/chunk-7BMKOD7N.js.map +0 -1
- package/dist/chunk-BTQREMD4.js +0 -127
- package/dist/chunk-BTQREMD4.js.map +0 -1
- package/dist/workspace-guard-524GBJQ6.js +0 -5
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/resource-caps/index.ts"],"names":["RESOURCE_CAP_DEFAULTS","workspace","free","paidDefault","scope","workspaceMember","sandbox","vibe","agent","conversation","message","tag","file","group","calendar","scheduledTask","form","apiKey","webhook","ResourceCapExceededError","GraphQLError","detail","resource","currentCount","limit","planName","extensions","code","name","PLAN_QUERY","ResourceCapsModule","client","cacheTtlMs","defaultTier","planCache","Map","config","getResourceCap","userId","Error","defaults","plan","resolvePlan","override","overrides","planTier","isFallback","enforceResourceCap","cap","invalidate","delete","invalidateAll","clear","cached","get","Date","now","fetchedAt","fetchPlan","set","fallbackPlan","res","query","planNode","userBillingPlan","tier","normaliseTier","sanitizeOverrides","resourceCaps","err","logger","warn","raw","fallback","lc","toLowerCase","out","key","value","Object","entries","Number","isFinite","Math","max","floor","defaultInstance","getResourceCaps","__resetResourceCapsForTests"],"mappings":"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","file":"chunk-7BMKOD7N.js","sourcesContent":["/**\n * Resource Caps Module\n *\n * Enforces per-user / per-workspace resource ceilings to prevent unbounded\n * resource use on a multi-tenant SaaS. Caps come from the user's billing plan\n * (resolved via global-billing-svc) and fall back to static defaults defined\n * here when the plan lookup fails or the resource isn't overridden.\n *\n * Two plan tiers exist by default:\n * - free: tight per-user / per-workspace ceilings\n * - paid (default): ~100x looser; individual paid plans can override via the\n * `Plan.resourceCaps` JSON column in global-billing-svc.\n *\n * @example\n * ```ts\n * import { resourceCaps } from '@burdenoff/be-sdk/resource-caps';\n *\n * const { limit } = await resourceCaps.getResourceCap(userId, 'tag');\n * await resourceCaps.enforceResourceCap(userId, 'tag', currentCount);\n * ```\n */\n\nimport { GraphQLError } from 'graphql';\n\nimport type { BurdenoffGraphQLClient } from '../client/graphql.js';\nimport { logger } from '../logger/index.js';\n\n// ---------------------------------------------------------------------------\n// Resource registry\n// ---------------------------------------------------------------------------\n\n/**\n * Canonical resource names. Adding a new capped resource? Append it here,\n * extend RESOURCE_CAP_DEFAULTS, and reference it from the relevant svc.\n */\nexport type ResourceName =\n | 'workspace' // per user\n | 'workspaceMember' // per workspace\n | 'sandbox' // per user\n | 'vibe' // per user\n | 'agent' // per workspace\n | 'conversation' // per user/day\n | 'message' // per user/day\n | 'tag' // per workspace\n | 'file' // per user (count only)\n | 'group' // per workspace\n | 'calendar' // per user\n | 'scheduledTask' // per user\n | 'form' // per user\n | 'apiKey' // per user\n | 'webhook'; // per workspace\n\nexport type PlanTier = 'free' | 'paid';\n\nexport interface ResourceCapDefaults {\n free: number;\n paidDefault: number;\n /** Optional scope hint used by callers when wiring guards */\n scope: 'user' | 'workspace' | 'user/day';\n}\n\n/**\n * Static fallback caps. Real values may be overridden per plan in\n * global-billing-svc Plan.resourceCaps JSON.\n */\nexport const RESOURCE_CAP_DEFAULTS: Record<ResourceName, ResourceCapDefaults> = {\n workspace: { free: 3, paidDefault: 50, scope: 'user' },\n workspaceMember: { free: 5, paidDefault: 500, scope: 'workspace' },\n sandbox: { free: 2, paidDefault: 50, scope: 'user' },\n vibe: { free: 10, paidDefault: 500, scope: 'user' },\n agent: { free: 2, paidDefault: 20, scope: 'workspace' },\n conversation: { free: 20, paidDefault: 2000, scope: 'user/day' },\n message: { free: 200, paidDefault: 20000, scope: 'user/day' },\n tag: { free: 50, paidDefault: 5000, scope: 'workspace' },\n file: { free: 100, paidDefault: 10000, scope: 'user' },\n group: { free: 5, paidDefault: 500, scope: 'workspace' },\n calendar: { free: 3, paidDefault: 100, scope: 'user' },\n scheduledTask: { free: 5, paidDefault: 500, scope: 'user' },\n form: { free: 5, paidDefault: 500, scope: 'user' },\n apiKey: { free: 2, paidDefault: 50, scope: 'user' },\n webhook: { free: 2, paidDefault: 100, scope: 'workspace' },\n};\n\n// ---------------------------------------------------------------------------\n// Types & errors\n// ---------------------------------------------------------------------------\n\nexport interface ResourceCap {\n resource: ResourceName;\n limit: number;\n planName: string;\n planTier: PlanTier;\n /** True when the cap came from defaults, not from the billing service */\n isFallback: boolean;\n}\n\nexport interface ResourceCapsConfig {\n client?: BurdenoffGraphQLClient;\n /** Optional in-memory TTL for plan lookups; defaults to 60s */\n cacheTtlMs?: number;\n /** Default plan tier when billing lookup fails */\n defaultTier?: PlanTier;\n}\n\ninterface CachedPlan {\n planName: string;\n planTier: PlanTier;\n overrides: Partial<Record<ResourceName, number>>;\n fetchedAt: number;\n}\n\n/**\n * Thrown when an entity create operation would push the user past the cap.\n * The matching GraphQL error has extensions.code = RESOURCE_CAP_EXCEEDED\n * so clients can detect and route to an upgrade flow.\n */\nexport class ResourceCapExceededError extends GraphQLError {\n constructor(detail: { resource: ResourceName; currentCount: number; limit: number; planName: string }) {\n super(\n `Resource cap exceeded for ${detail.resource}: ${detail.currentCount}/${detail.limit} on plan '${detail.planName}'.`,\n {\n extensions: {\n code: 'RESOURCE_CAP_EXCEEDED',\n resource: detail.resource,\n currentCount: detail.currentCount,\n limit: detail.limit,\n planName: detail.planName,\n },\n }\n );\n this.name = 'ResourceCapExceededError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Module\n// ---------------------------------------------------------------------------\n\nconst PLAN_QUERY = /* GraphQL */ `\n query GetUserPlanForCaps($userId: ID!) {\n userBillingPlan(userId: $userId) {\n planName\n tier\n resourceCaps\n }\n }\n`;\n\ninterface PlanQueryResponse {\n userBillingPlan?: {\n planName?: string | null;\n tier?: string | null;\n resourceCaps?: Record<string, number> | null;\n } | null;\n}\n\nexport class ResourceCapsModule {\n private readonly client?: BurdenoffGraphQLClient;\n private readonly cacheTtlMs: number;\n private readonly defaultTier: PlanTier;\n private readonly planCache = new Map<string, CachedPlan>();\n\n constructor(config: ResourceCapsConfig = {}) {\n this.client = config.client;\n this.cacheTtlMs = config.cacheTtlMs ?? 60_000;\n this.defaultTier = config.defaultTier ?? 'free';\n }\n\n /**\n * Resolve the cap for a given resource for the supplied user.\n * Order: plan-override → plan-tier-default → static free default.\n */\n async getResourceCap(userId: string, resource: ResourceName): Promise<ResourceCap> {\n if (!userId) {\n throw new Error('getResourceCap: userId is required');\n }\n const defaults = RESOURCE_CAP_DEFAULTS[resource];\n if (!defaults) {\n throw new Error(`getResourceCap: unknown resource '${resource}'`);\n }\n\n const plan = await this.resolvePlan(userId);\n const override = plan.overrides[resource];\n if (typeof override === 'number' && override >= 0) {\n return {\n resource,\n limit: override,\n planName: plan.planName,\n planTier: plan.planTier,\n isFallback: false,\n };\n }\n\n const limit = plan.planTier === 'paid' ? defaults.paidDefault : defaults.free;\n return {\n resource,\n limit,\n planName: plan.planName,\n planTier: plan.planTier,\n isFallback: plan.planName === 'fallback-free' || plan.planName === 'fallback-paid',\n };\n }\n\n /**\n * Throws ResourceCapExceededError when currentCount >= limit.\n * Use BEFORE the create transaction to avoid orphan rows.\n */\n async enforceResourceCap(userId: string, resource: ResourceName, currentCount: number): Promise<ResourceCap> {\n const cap = await this.getResourceCap(userId, resource);\n if (currentCount >= cap.limit) {\n throw new ResourceCapExceededError({\n resource,\n currentCount,\n limit: cap.limit,\n planName: cap.planName,\n });\n }\n return cap;\n }\n\n /** Invalidate cache for a user (e.g. after plan upgrade) */\n invalidate(userId: string): void {\n this.planCache.delete(userId);\n }\n\n /** Clear entire plan cache */\n invalidateAll(): void {\n this.planCache.clear();\n }\n\n // -------------------------------------------------------------------------\n // Internals\n // -------------------------------------------------------------------------\n\n private async resolvePlan(userId: string): Promise<CachedPlan> {\n const cached = this.planCache.get(userId);\n if (cached && Date.now() - cached.fetchedAt < this.cacheTtlMs) {\n return cached;\n }\n\n const plan = await this.fetchPlan(userId);\n this.planCache.set(userId, plan);\n return plan;\n }\n\n private async fetchPlan(userId: string): Promise<CachedPlan> {\n if (!this.client) {\n return this.fallbackPlan();\n }\n try {\n const res = await this.client.query<PlanQueryResponse>(PLAN_QUERY, { userId });\n const planNode = res?.userBillingPlan;\n if (!planNode) {\n return this.fallbackPlan();\n }\n const tier = normaliseTier(planNode.tier, this.defaultTier);\n const overrides = sanitizeOverrides(planNode.resourceCaps);\n return {\n planName: planNode.planName ?? (tier === 'paid' ? 'paid' : 'free'),\n planTier: tier,\n overrides,\n fetchedAt: Date.now(),\n };\n } catch (err) {\n logger.warn('resource-caps: plan lookup failed; falling back to default tier', {\n err,\n userId,\n });\n return this.fallbackPlan();\n }\n }\n\n private fallbackPlan(): CachedPlan {\n return {\n planName: this.defaultTier === 'paid' ? 'fallback-paid' : 'fallback-free',\n planTier: this.defaultTier,\n overrides: {},\n fetchedAt: Date.now(),\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction normaliseTier(raw: string | null | undefined, fallback: PlanTier): PlanTier {\n if (!raw) return fallback;\n const lc = raw.toLowerCase();\n if (lc === 'free' || lc === 'paid') return lc;\n // Treat anything non-free as paid (enterprise, business, pro, etc.)\n if (lc === 'enterprise' || lc === 'business' || lc === 'pro' || lc === 'team') {\n return 'paid';\n }\n return fallback;\n}\n\nfunction sanitizeOverrides(raw: Record<string, number> | null | undefined): Partial<Record<ResourceName, number>> {\n if (!raw || typeof raw !== 'object') return {};\n const out: Partial<Record<ResourceName, number>> = {};\n for (const [key, value] of Object.entries(raw)) {\n if (key in RESOURCE_CAP_DEFAULTS && typeof value === 'number' && Number.isFinite(value)) {\n out[key as ResourceName] = Math.max(0, Math.floor(value));\n }\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Convenience singleton\n// ---------------------------------------------------------------------------\n\nlet defaultInstance: ResourceCapsModule | null = null;\n\n/**\n * Obtain a process-wide singleton. The first call binds the GraphQL client\n * (typically the SDK's internal client). Later calls return the same instance.\n */\nexport function getResourceCaps(config?: ResourceCapsConfig): ResourceCapsModule {\n if (!defaultInstance) {\n defaultInstance = new ResourceCapsModule(config);\n }\n return defaultInstance;\n}\n\n/** Reset the singleton (for tests) */\nexport function __resetResourceCapsForTests(): void {\n defaultInstance = null;\n}\n"]}
|
package/dist/chunk-BTQREMD4.js
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { logger } from './chunk-CBZNP6RF.js';
|
|
2
|
-
import { __name } from './chunk-XE4OXN2W.js';
|
|
3
|
-
|
|
4
|
-
// src/server/workspace-guard.ts
|
|
5
|
-
var WorkspaceRequiredError = class WorkspaceRequiredError2 extends Error {
|
|
6
|
-
static {
|
|
7
|
-
__name(this, "WorkspaceRequiredError");
|
|
8
|
-
}
|
|
9
|
-
extensions;
|
|
10
|
-
constructor(message) {
|
|
11
|
-
super(message);
|
|
12
|
-
this.name = "WorkspaceRequiredError";
|
|
13
|
-
this.extensions = {
|
|
14
|
-
code: "WORKSPACE_REQUIRED",
|
|
15
|
-
http: {
|
|
16
|
-
status: 403
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
function isFieldPublic(field) {
|
|
22
|
-
const directives = field?.astNode?.directives;
|
|
23
|
-
if (!directives || directives.length === 0) return false;
|
|
24
|
-
for (const dir of directives) {
|
|
25
|
-
const name = dir.name?.value;
|
|
26
|
-
if (name === "public") return true;
|
|
27
|
-
if (name === "rbac") {
|
|
28
|
-
const args = dir.arguments ?? [];
|
|
29
|
-
for (const arg of args) {
|
|
30
|
-
if (arg.name?.value === "public" && arg.value?.kind === "BooleanValue" && arg.value.value === true) {
|
|
31
|
-
return true;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
__name(isFieldPublic, "isFieldPublic");
|
|
39
|
-
function resolveOperationType(document, operationName) {
|
|
40
|
-
const defs = document?.definitions ?? [];
|
|
41
|
-
for (const def of defs) {
|
|
42
|
-
if (def.kind !== "OperationDefinition") ;
|
|
43
|
-
if (def.operation && (!operationName || def.name?.value === operationName)) {
|
|
44
|
-
return def.operation;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return "query";
|
|
48
|
-
}
|
|
49
|
-
__name(resolveOperationType, "resolveOperationType");
|
|
50
|
-
function allRootFieldsPublic(schema, document, operationName) {
|
|
51
|
-
if (!schema || !document) return false;
|
|
52
|
-
const opType = resolveOperationType(document, operationName);
|
|
53
|
-
let rootType;
|
|
54
|
-
if (opType === "mutation") {
|
|
55
|
-
rootType = schema.getMutationType?.();
|
|
56
|
-
} else if (opType === "subscription") {
|
|
57
|
-
rootType = schema.getSubscriptionType?.();
|
|
58
|
-
} else {
|
|
59
|
-
rootType = schema.getQueryType?.();
|
|
60
|
-
}
|
|
61
|
-
if (!rootType) return false;
|
|
62
|
-
const fields = rootType.getFields();
|
|
63
|
-
const defs = document.definitions ?? [];
|
|
64
|
-
const opDef = defs.find((d) => d.operation && (!operationName || d.name?.value === operationName));
|
|
65
|
-
const selections = opDef?.selectionSet?.selections ?? [];
|
|
66
|
-
if (selections.length === 0) return false;
|
|
67
|
-
for (const sel of selections) {
|
|
68
|
-
if (sel.kind && sel.kind !== "Field") return false;
|
|
69
|
-
const fieldName = sel.name?.value;
|
|
70
|
-
if (!fieldName) return false;
|
|
71
|
-
if (fieldName.startsWith("__")) continue;
|
|
72
|
-
const fieldDef = fields[fieldName];
|
|
73
|
-
if (!isFieldPublic(fieldDef)) return false;
|
|
74
|
-
}
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
__name(allRootFieldsPublic, "allRootFieldsPublic");
|
|
78
|
-
function createWorkspaceGuardPlugin(config) {
|
|
79
|
-
const enabled = config?.enabled ?? true;
|
|
80
|
-
const whitelistedOperations = new Set(config?.whitelistedOperations ?? []);
|
|
81
|
-
const allowPublicDirective = config?.allowPublicDirective ?? true;
|
|
82
|
-
const errorMessage = config?.errorMessage ?? "Workspace context required. Ensure the request includes a valid workspace context (x-workspace-id header).";
|
|
83
|
-
if (!enabled) {
|
|
84
|
-
logger.info("[WorkspaceGuard] Workspace guard disabled");
|
|
85
|
-
return {};
|
|
86
|
-
}
|
|
87
|
-
logger.info("[WorkspaceGuard] Workspace guard enabled", {
|
|
88
|
-
whitelistedOperations: [
|
|
89
|
-
...whitelistedOperations
|
|
90
|
-
],
|
|
91
|
-
allowPublicDirective
|
|
92
|
-
});
|
|
93
|
-
return {
|
|
94
|
-
onExecute({ args }) {
|
|
95
|
-
let operationName = args.operationName;
|
|
96
|
-
if (!operationName && args.document?.definitions) {
|
|
97
|
-
for (const def of args.document.definitions) {
|
|
98
|
-
if (def.operation && def.name?.value) {
|
|
99
|
-
operationName = def.name.value;
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
if (operationName && whitelistedOperations.has(operationName)) {
|
|
105
|
-
return void 0;
|
|
106
|
-
}
|
|
107
|
-
if (allowPublicDirective && allRootFieldsPublic(args.schema, args.document, operationName)) {
|
|
108
|
-
return void 0;
|
|
109
|
-
}
|
|
110
|
-
const context = args.contextValue;
|
|
111
|
-
const workspaceId = context?.workspace?.id;
|
|
112
|
-
const headerWorkspaceId = context?.request?.headers?.get?.("x-workspace-id");
|
|
113
|
-
if (!workspaceId && !headerWorkspaceId) {
|
|
114
|
-
logger.warn("[WorkspaceGuard] Blocked operation without workspace context", {
|
|
115
|
-
operationName: operationName ?? "<anonymous>"
|
|
116
|
-
});
|
|
117
|
-
throw new WorkspaceRequiredError(errorMessage);
|
|
118
|
-
}
|
|
119
|
-
return void 0;
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
__name(createWorkspaceGuardPlugin, "createWorkspaceGuardPlugin");
|
|
124
|
-
|
|
125
|
-
export { createWorkspaceGuardPlugin };
|
|
126
|
-
//# sourceMappingURL=chunk-BTQREMD4.js.map
|
|
127
|
-
//# sourceMappingURL=chunk-BTQREMD4.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server/workspace-guard.ts"],"names":["WorkspaceRequiredError","Error","extensions","message","name","code","http","status","isFieldPublic","field","directives","astNode","length","dir","value","args","arguments","arg","kind","resolveOperationType","document","operationName","defs","definitions","def","operation","allRootFieldsPublic","schema","opType","rootType","getMutationType","getSubscriptionType","getQueryType","fields","getFields","opDef","find","d","selections","selectionSet","sel","fieldName","startsWith","fieldDef","createWorkspaceGuardPlugin","config","enabled","whitelistedOperations","Set","allowPublicDirective","errorMessage","logger","info","onExecute","has","undefined","context","contextValue","workspaceId","workspace","id","headerWorkspaceId","request","headers","get","warn"],"mappings":"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","file":"chunk-BTQREMD4.js","sourcesContent":["/**\n * Workspace Guard Plugin for GraphQL Yoga\n *\n * Blocks GraphQL operations that lack a valid workspace context (x-workspace-id header).\n * This provides defense-in-depth at the GraphQL layer — even if the gateway allows a request\n * through, the service itself will reject it if workspace context is missing.\n *\n * **Whitelisted operations** (e.g., workspace creation) can bypass the guard.\n *\n * **Public schema fields** marked with `@public` or `@rbac(public: true)` are auto-bypassed\n * when `allowPublicDirective` is enabled (default true). This lets anonymous public-link\n * resolvers (e.g. `accessShare`) be reached through the federation gateway without an\n * `x-workspace-id` header.\n *\n * @module server/workspace-guard\n * @category Security\n */\n\nimport { logger } from '../logger/index.js';\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Configuration for the workspace guard plugin\n */\nexport interface WorkspaceGuardConfig {\n /**\n * Enable the workspace guard.\n * @default true\n */\n enabled?: boolean;\n\n /**\n * GraphQL operation names that can run without workspace context.\n * These are typically operations like `createWorkspace` that execute\n * before a workspace exists.\n *\n * Matching is case-sensitive against the operation name in the GraphQL document.\n *\n * @default []\n *\n * @example\n * ```typescript\n * whitelistedOperations: ['createWorkspace', 'validateTokens']\n * ```\n */\n whitelistedOperations?: string[];\n\n /**\n * Custom error message when workspace context is missing.\n * @default 'Workspace context required. Provide a valid X-Workspace-Authorization header.'\n */\n errorMessage?: string;\n\n /**\n * If true, fields whose schema definition carries the `@public` directive or\n * `@rbac(public: true)` are bypassed by the guard automatically — no need to\n * list them in `whitelistedOperations`. The check uses the GraphQL schema\n * passed to the executor at request time.\n *\n * Set to false to fall back to the legacy operation-name allowlist only.\n *\n * @default true\n */\n allowPublicDirective?: boolean;\n}\n\n// ============================================================================\n// GraphQL Error\n// ============================================================================\n\n/**\n * Workspace required error with proper GraphQL error extensions\n */\nclass WorkspaceRequiredError extends Error {\n public readonly extensions: { code: string; http: { status: number } };\n\n constructor(message: string) {\n super(message);\n this.name = 'WorkspaceRequiredError';\n this.extensions = {\n code: 'WORKSPACE_REQUIRED',\n http: { status: 403 },\n };\n }\n}\n\n// ============================================================================\n// Schema directive helpers\n// ============================================================================\n\n/**\n * Minimal structural typings for the bits of the GraphQL schema we look at.\n * We avoid importing the GraphQL types directly so the SDK keeps `graphql`\n * as a peer dep without a runtime hard dependency.\n */\ninterface FieldLikeDef {\n astNode?: {\n directives?: ReadonlyArray<{\n name: { value: string };\n arguments?: ReadonlyArray<{\n name: { value: string };\n value: { kind: string; value?: unknown };\n }>;\n }>;\n };\n}\n\ninterface SchemaLike {\n getQueryType?: () => { getFields: () => Record<string, FieldLikeDef> } | null | undefined;\n getMutationType?: () => { getFields: () => Record<string, FieldLikeDef> } | null | undefined;\n getSubscriptionType?: () => { getFields: () => Record<string, FieldLikeDef> } | null | undefined;\n}\n\n/**\n * Returns true iff the field carries either:\n * - `@public` directive, OR\n * - `@rbac(public: true)` directive (any arg-order)\n */\nfunction isFieldPublic(field: FieldLikeDef | undefined): boolean {\n const directives = field?.astNode?.directives;\n if (!directives || directives.length === 0) return false;\n\n for (const dir of directives) {\n const name = dir.name?.value;\n if (name === 'public') return true;\n if (name === 'rbac') {\n const args = dir.arguments ?? [];\n for (const arg of args) {\n if (arg.name?.value === 'public' && arg.value?.kind === 'BooleanValue' && arg.value.value === true) {\n return true;\n }\n }\n }\n }\n return false;\n}\n\ninterface DocumentLike {\n definitions?: ReadonlyArray<{\n kind?: string;\n operation?: string;\n name?: { value?: string };\n selectionSet?: {\n selections?: ReadonlyArray<{\n kind?: string;\n name?: { value?: string };\n }>;\n };\n }>;\n}\n\n/**\n * Resolve the operation type ('query' | 'mutation' | 'subscription') for the\n * primary operation in the document. Falls back to 'query'.\n */\nfunction resolveOperationType(\n document: DocumentLike | undefined,\n operationName: string | null | undefined\n): 'query' | 'mutation' | 'subscription' {\n const defs = document?.definitions ?? [];\n for (const def of defs) {\n if (def.kind !== 'OperationDefinition') {\n // Older parsers may not set kind; fall through to the operation property check below.\n }\n if (def.operation && (!operationName || def.name?.value === operationName)) {\n return def.operation as 'query' | 'mutation' | 'subscription';\n }\n }\n return 'query';\n}\n\n/**\n * Returns true iff every root field in the document selection set is marked\n * public on the schema. Empty selection sets return false (fail-closed).\n */\nfunction allRootFieldsPublic(\n schema: SchemaLike | undefined,\n document: DocumentLike | undefined,\n operationName: string | null | undefined\n): boolean {\n if (!schema || !document) return false;\n\n const opType = resolveOperationType(document, operationName);\n let rootType: ReturnType<NonNullable<SchemaLike['getQueryType']>> | undefined;\n if (opType === 'mutation') {\n rootType = schema.getMutationType?.();\n } else if (opType === 'subscription') {\n rootType = schema.getSubscriptionType?.();\n } else {\n rootType = schema.getQueryType?.();\n }\n\n if (!rootType) return false;\n\n const fields = rootType.getFields();\n const defs = document.definitions ?? [];\n\n // Find the operation definition matching operationName (or first one).\n const opDef = defs.find((d) => d.operation && (!operationName || d.name?.value === operationName));\n const selections = opDef?.selectionSet?.selections ?? [];\n if (selections.length === 0) return false;\n\n for (const sel of selections) {\n // Only consider plain Field selections (skip fragments — fragments at the root\n // shouldn't drive a public bypass; fail-closed there).\n if (sel.kind && sel.kind !== 'Field') return false;\n const fieldName = sel.name?.value;\n if (!fieldName) return false;\n // __schema / __type introspection is always allowed without workspace context;\n // treat them as public.\n if (fieldName.startsWith('__')) continue;\n const fieldDef = fields[fieldName];\n if (!isFieldPublic(fieldDef)) return false;\n }\n return true;\n}\n\n// ============================================================================\n// Plugin\n// ============================================================================\n\n/**\n * Create a GraphQL Yoga plugin that guards operations requiring workspace context.\n *\n * The plugin inspects the GraphQL context for workspace information (set by the SDK's\n * buildContext from the x-workspace-id header). If missing and the operation is not\n * whitelisted (and not all-public per directive), it throws a WORKSPACE_REQUIRED error.\n *\n * @param config - Guard configuration\n * @returns A Yoga-compatible plugin\n *\n * @example\n * ```typescript\n * import { createWorkspaceGuardPlugin } from '@burdenoff/be-sdk/server';\n *\n * const guardPlugin = createWorkspaceGuardPlugin({\n * whitelistedOperations: ['createWorkspace'],\n * allowPublicDirective: true, // default\n * });\n * ```\n */\nexport function createWorkspaceGuardPlugin(config?: WorkspaceGuardConfig) {\n const enabled = config?.enabled ?? true;\n const whitelistedOperations = new Set(config?.whitelistedOperations ?? []);\n const allowPublicDirective = config?.allowPublicDirective ?? true;\n const errorMessage =\n config?.errorMessage ??\n 'Workspace context required. Ensure the request includes a valid workspace context (x-workspace-id header).';\n\n if (!enabled) {\n logger.info('[WorkspaceGuard] Workspace guard disabled');\n return {};\n }\n\n logger.info('[WorkspaceGuard] Workspace guard enabled', {\n whitelistedOperations: [...whitelistedOperations],\n allowPublicDirective,\n });\n\n return {\n onExecute({\n args,\n }: {\n args: {\n schema?: SchemaLike;\n document?: DocumentLike;\n operationName?: string | null;\n contextValue?: { workspace?: { id?: string }; request?: Request };\n };\n }) {\n // Extract operation name from args or document\n let operationName = args.operationName;\n\n if (!operationName && args.document?.definitions) {\n for (const def of args.document.definitions) {\n if (def.operation && def.name?.value) {\n operationName = def.name.value;\n break;\n }\n }\n }\n\n // Check if operation is whitelisted by name\n if (operationName && whitelistedOperations.has(operationName)) {\n return undefined;\n }\n\n // Check schema-directive-based public bypass (Option C)\n if (allowPublicDirective && allRootFieldsPublic(args.schema, args.document, operationName)) {\n return undefined;\n }\n\n // Check for workspace context\n const context = args.contextValue;\n const workspaceId = context?.workspace?.id;\n\n // Also check the raw header as a fallback\n const headerWorkspaceId = context?.request?.headers?.get?.('x-workspace-id');\n\n if (!workspaceId && !headerWorkspaceId) {\n logger.warn('[WorkspaceGuard] Blocked operation without workspace context', {\n operationName: operationName ?? '<anonymous>',\n });\n throw new WorkspaceRequiredError(errorMessage);\n }\n\n return undefined;\n },\n };\n}\n"]}
|