@classytic/arc 1.0.0
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 +21 -0
- package/README.md +900 -0
- package/bin/arc.js +344 -0
- package/dist/adapters/index.d.ts +237 -0
- package/dist/adapters/index.js +668 -0
- package/dist/arcCorePlugin-DTPWXcZN.d.ts +273 -0
- package/dist/audit/index.d.ts +195 -0
- package/dist/audit/index.js +319 -0
- package/dist/auth/index.d.ts +47 -0
- package/dist/auth/index.js +174 -0
- package/dist/cli/commands/docs.d.ts +11 -0
- package/dist/cli/commands/docs.js +474 -0
- package/dist/cli/commands/introspect.d.ts +8 -0
- package/dist/cli/commands/introspect.js +338 -0
- package/dist/cli/index.d.ts +43 -0
- package/dist/cli/index.js +520 -0
- package/dist/createApp-pzUAkzbz.d.ts +77 -0
- package/dist/docs/index.d.ts +166 -0
- package/dist/docs/index.js +650 -0
- package/dist/errors-8WIxGS_6.d.ts +122 -0
- package/dist/events/index.d.ts +117 -0
- package/dist/events/index.js +89 -0
- package/dist/factory/index.d.ts +38 -0
- package/dist/factory/index.js +1664 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +199 -0
- package/dist/idempotency/index.d.ts +323 -0
- package/dist/idempotency/index.js +500 -0
- package/dist/index-DkAW8BXh.d.ts +1302 -0
- package/dist/index.d.ts +331 -0
- package/dist/index.js +4734 -0
- package/dist/migrations/index.d.ts +185 -0
- package/dist/migrations/index.js +274 -0
- package/dist/org/index.d.ts +129 -0
- package/dist/org/index.js +220 -0
- package/dist/permissions/index.d.ts +144 -0
- package/dist/permissions/index.js +100 -0
- package/dist/plugins/index.d.ts +46 -0
- package/dist/plugins/index.js +1069 -0
- package/dist/policies/index.d.ts +398 -0
- package/dist/policies/index.js +196 -0
- package/dist/presets/index.d.ts +336 -0
- package/dist/presets/index.js +382 -0
- package/dist/presets/multiTenant.d.ts +39 -0
- package/dist/presets/multiTenant.js +112 -0
- package/dist/registry/index.d.ts +16 -0
- package/dist/registry/index.js +253 -0
- package/dist/testing/index.d.ts +618 -0
- package/dist/testing/index.js +48032 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.js +8 -0
- package/dist/types-0IPhH_NR.d.ts +143 -0
- package/dist/types-B99TBmFV.d.ts +76 -0
- package/dist/utils/index.d.ts +655 -0
- package/dist/utils/index.js +905 -0
- package/package.json +227 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { R as RequestWithExtras, C as CrudRouteKey, P as PresetResult } from '../index-DkAW8BXh.js';
|
|
2
|
+
import 'mongoose';
|
|
3
|
+
import 'fastify';
|
|
4
|
+
import '../types-B99TBmFV.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Multi-Tenant Preset
|
|
8
|
+
*
|
|
9
|
+
* Adds tenant (organization) filtering and injection middlewares.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
interface MultiTenantOptions {
|
|
13
|
+
/** Field name in database (default: 'organizationId') */
|
|
14
|
+
tenantField?: string;
|
|
15
|
+
/** Roles that bypass tenant isolation (default: ['superadmin']) */
|
|
16
|
+
bypassRoles?: string[];
|
|
17
|
+
/**
|
|
18
|
+
* Custom function to extract organizationId from request
|
|
19
|
+
* If not provided, tries in order:
|
|
20
|
+
* 1. request.context.organizationId
|
|
21
|
+
* 2. request.user.organizationId
|
|
22
|
+
* 3. request.user.organization
|
|
23
|
+
*/
|
|
24
|
+
extractOrganizationId?: (request: RequestWithExtras) => string | null | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Routes that allow public access (no auth required)
|
|
27
|
+
* When a route is in this array:
|
|
28
|
+
* - If no org context: allow through without filtering (public data)
|
|
29
|
+
* - If org context present: require auth and apply filter
|
|
30
|
+
*
|
|
31
|
+
* @default [] (strict mode - all routes require auth)
|
|
32
|
+
* @example
|
|
33
|
+
* multiTenantPreset({ allowPublic: ['list', 'get'] })
|
|
34
|
+
*/
|
|
35
|
+
allowPublic?: CrudRouteKey[];
|
|
36
|
+
}
|
|
37
|
+
declare function multiTenantPreset(options?: MultiTenantOptions): PresetResult;
|
|
38
|
+
|
|
39
|
+
export { type MultiTenantOptions, multiTenantPreset as default, multiTenantPreset };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// src/presets/multiTenant.ts
|
|
2
|
+
function defaultExtractOrganizationId(request) {
|
|
3
|
+
const context = request.context;
|
|
4
|
+
if (context?.organizationId) {
|
|
5
|
+
return context.organizationId;
|
|
6
|
+
}
|
|
7
|
+
const user = request.user;
|
|
8
|
+
if (user?.organizationId) {
|
|
9
|
+
return user.organizationId;
|
|
10
|
+
}
|
|
11
|
+
if (user?.organization) {
|
|
12
|
+
const org = user.organization;
|
|
13
|
+
return org._id || org.id || org;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
function createTenantFilter(tenantField, bypassRoles, extractOrganizationId) {
|
|
18
|
+
return async (request, reply) => {
|
|
19
|
+
const user = request.user;
|
|
20
|
+
if (!user) {
|
|
21
|
+
reply.code(401).send({
|
|
22
|
+
success: false,
|
|
23
|
+
error: "Unauthorized",
|
|
24
|
+
message: "Authentication required for multi-tenant resources"
|
|
25
|
+
});
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const userWithRoles = user;
|
|
29
|
+
if (userWithRoles.roles && bypassRoles.some((r) => userWithRoles.roles.includes(r))) return;
|
|
30
|
+
const orgId = extractOrganizationId(request);
|
|
31
|
+
if (!orgId) {
|
|
32
|
+
reply.code(403).send({
|
|
33
|
+
success: false,
|
|
34
|
+
error: "Forbidden",
|
|
35
|
+
message: "Organization context required for this operation"
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
request.query = request.query ?? {};
|
|
40
|
+
request.query._policyFilters = {
|
|
41
|
+
...request.query._policyFilters ?? {},
|
|
42
|
+
[tenantField]: orgId
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function createFlexibleTenantFilter(tenantField, bypassRoles, extractOrganizationId) {
|
|
47
|
+
return async (request, reply) => {
|
|
48
|
+
const user = request.user;
|
|
49
|
+
const orgId = extractOrganizationId(request);
|
|
50
|
+
if (!orgId) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!user) {
|
|
54
|
+
reply.code(401).send({
|
|
55
|
+
success: false,
|
|
56
|
+
error: "Unauthorized",
|
|
57
|
+
message: "Authentication required for organization-scoped data"
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const userWithRoles = user;
|
|
62
|
+
if (userWithRoles.roles && bypassRoles.some((r) => userWithRoles.roles.includes(r))) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
request.query = request.query ?? {};
|
|
66
|
+
request.query._policyFilters = {
|
|
67
|
+
...request.query._policyFilters ?? {},
|
|
68
|
+
[tenantField]: orgId
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function createTenantInjection(tenantField, extractOrganizationId) {
|
|
73
|
+
return async (request, reply) => {
|
|
74
|
+
const orgId = extractOrganizationId(request);
|
|
75
|
+
if (!orgId) {
|
|
76
|
+
reply.code(403).send({
|
|
77
|
+
success: false,
|
|
78
|
+
error: "Forbidden",
|
|
79
|
+
message: "Organization context required to create resources"
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (request.body) {
|
|
84
|
+
request.body[tenantField] = orgId;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function multiTenantPreset(options = {}) {
|
|
89
|
+
const {
|
|
90
|
+
tenantField = "organizationId",
|
|
91
|
+
bypassRoles = ["superadmin"],
|
|
92
|
+
extractOrganizationId = defaultExtractOrganizationId,
|
|
93
|
+
allowPublic = []
|
|
94
|
+
} = options;
|
|
95
|
+
const strictTenantFilter = createTenantFilter(tenantField, bypassRoles, extractOrganizationId);
|
|
96
|
+
const flexibleTenantFilter = createFlexibleTenantFilter(tenantField, bypassRoles, extractOrganizationId);
|
|
97
|
+
const tenantInjection = createTenantInjection(tenantField, extractOrganizationId);
|
|
98
|
+
const getFilter = (route) => allowPublic.includes(route) ? flexibleTenantFilter : strictTenantFilter;
|
|
99
|
+
return {
|
|
100
|
+
name: "multiTenant",
|
|
101
|
+
middlewares: {
|
|
102
|
+
list: [getFilter("list")],
|
|
103
|
+
get: [getFilter("get")],
|
|
104
|
+
create: [tenantInjection],
|
|
105
|
+
update: [getFilter("update")],
|
|
106
|
+
delete: [getFilter("delete")]
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
var multiTenant_default = multiTenantPreset;
|
|
111
|
+
|
|
112
|
+
export { multiTenant_default as default, multiTenantPreset };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { I as IntrospectionPluginOptions } from '../index-DkAW8BXh.js';
|
|
2
|
+
export { c as RegisterOptions, b as ResourceRegistry, r as resourceRegistry } from '../index-DkAW8BXh.js';
|
|
3
|
+
import { FastifyPluginAsync } from 'fastify';
|
|
4
|
+
import 'mongoose';
|
|
5
|
+
import '../types-B99TBmFV.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Introspection Plugin
|
|
9
|
+
*
|
|
10
|
+
* Exposes resource registry via API endpoints.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
declare const introspectionPlugin: FastifyPluginAsync<IntrospectionPluginOptions>;
|
|
14
|
+
declare const _default: FastifyPluginAsync<IntrospectionPluginOptions>;
|
|
15
|
+
|
|
16
|
+
export { IntrospectionPluginOptions, _default as introspectionPlugin, introspectionPlugin as introspectionPluginFn };
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import fp from 'fastify-plugin';
|
|
2
|
+
|
|
3
|
+
// src/registry/ResourceRegistry.ts
|
|
4
|
+
var ResourceRegistry = class {
|
|
5
|
+
_resources;
|
|
6
|
+
_frozen;
|
|
7
|
+
constructor() {
|
|
8
|
+
this._resources = /* @__PURE__ */ new Map();
|
|
9
|
+
this._frozen = false;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Register a resource
|
|
13
|
+
*/
|
|
14
|
+
register(resource, options = {}) {
|
|
15
|
+
if (this._frozen) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`Registry frozen. Cannot register '${resource.name}' after startup.`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
if (this._resources.has(resource.name)) {
|
|
21
|
+
throw new Error(`Resource '${resource.name}' already registered.`);
|
|
22
|
+
}
|
|
23
|
+
const entry = {
|
|
24
|
+
name: resource.name,
|
|
25
|
+
displayName: resource.displayName,
|
|
26
|
+
tag: resource.tag,
|
|
27
|
+
prefix: resource.prefix,
|
|
28
|
+
module: options.module ?? void 0,
|
|
29
|
+
adapter: resource.adapter ? {
|
|
30
|
+
type: resource.adapter.type,
|
|
31
|
+
name: resource.adapter.name
|
|
32
|
+
} : null,
|
|
33
|
+
permissions: resource.permissions,
|
|
34
|
+
presets: resource._appliedPresets ?? [],
|
|
35
|
+
routes: [],
|
|
36
|
+
// Populated later by getIntrospection()
|
|
37
|
+
additionalRoutes: resource.additionalRoutes.map((r) => ({
|
|
38
|
+
method: r.method,
|
|
39
|
+
path: r.path,
|
|
40
|
+
handler: typeof r.handler === "string" ? r.handler : r.handler.name || "anonymous",
|
|
41
|
+
summary: r.summary,
|
|
42
|
+
description: r.description,
|
|
43
|
+
permissions: r.permissions,
|
|
44
|
+
wrapHandler: r.wrapHandler,
|
|
45
|
+
schema: r.schema
|
|
46
|
+
// Include schema for OpenAPI docs
|
|
47
|
+
})),
|
|
48
|
+
events: Object.keys(resource.events ?? {}),
|
|
49
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
50
|
+
disableDefaultRoutes: resource.disableDefaultRoutes,
|
|
51
|
+
openApiSchemas: options.openApiSchemas,
|
|
52
|
+
plugin: resource.toPlugin()
|
|
53
|
+
// Store plugin factory
|
|
54
|
+
};
|
|
55
|
+
this._resources.set(resource.name, entry);
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get resource by name
|
|
60
|
+
*/
|
|
61
|
+
get(name) {
|
|
62
|
+
return this._resources.get(name);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get all resources
|
|
66
|
+
*/
|
|
67
|
+
getAll() {
|
|
68
|
+
return Array.from(this._resources.values());
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get resources by module
|
|
72
|
+
*/
|
|
73
|
+
getByModule(moduleName) {
|
|
74
|
+
return this.getAll().filter((r) => r.module === moduleName);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get resources by preset
|
|
78
|
+
*/
|
|
79
|
+
getByPreset(presetName) {
|
|
80
|
+
return this.getAll().filter((r) => r.presets.includes(presetName));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Check if resource exists
|
|
84
|
+
*/
|
|
85
|
+
has(name) {
|
|
86
|
+
return this._resources.has(name);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get registry statistics
|
|
90
|
+
*/
|
|
91
|
+
getStats() {
|
|
92
|
+
const resources = this.getAll();
|
|
93
|
+
const presetCounts = {};
|
|
94
|
+
for (const r of resources) {
|
|
95
|
+
for (const preset of r.presets) {
|
|
96
|
+
presetCounts[preset] = (presetCounts[preset] ?? 0) + 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
totalResources: resources.length,
|
|
101
|
+
byModule: this._groupBy(resources, "module"),
|
|
102
|
+
presetUsage: presetCounts,
|
|
103
|
+
totalRoutes: resources.reduce((sum, r) => {
|
|
104
|
+
const defaultRouteCount = r.disableDefaultRoutes ? 0 : 5;
|
|
105
|
+
return sum + (r.additionalRoutes?.length ?? 0) + defaultRouteCount;
|
|
106
|
+
}, 0),
|
|
107
|
+
totalEvents: resources.reduce((sum, r) => sum + (r.events?.length ?? 0), 0)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get full introspection data
|
|
112
|
+
*/
|
|
113
|
+
getIntrospection() {
|
|
114
|
+
return {
|
|
115
|
+
resources: this.getAll().map((r) => {
|
|
116
|
+
const defaultRoutes = r.disableDefaultRoutes ? [] : [
|
|
117
|
+
{ method: "GET", path: r.prefix, operation: "list" },
|
|
118
|
+
{ method: "GET", path: `${r.prefix}/:id`, operation: "get" },
|
|
119
|
+
{ method: "POST", path: r.prefix, operation: "create" },
|
|
120
|
+
{ method: "PATCH", path: `${r.prefix}/:id`, operation: "update" },
|
|
121
|
+
{ method: "DELETE", path: `${r.prefix}/:id`, operation: "delete" }
|
|
122
|
+
];
|
|
123
|
+
return {
|
|
124
|
+
name: r.name,
|
|
125
|
+
displayName: r.displayName,
|
|
126
|
+
prefix: r.prefix,
|
|
127
|
+
module: r.module,
|
|
128
|
+
presets: r.presets,
|
|
129
|
+
permissions: r.permissions,
|
|
130
|
+
routes: [
|
|
131
|
+
...defaultRoutes,
|
|
132
|
+
...r.additionalRoutes?.map((ar) => ({
|
|
133
|
+
method: ar.method,
|
|
134
|
+
path: `${r.prefix}${ar.path}`,
|
|
135
|
+
operation: typeof ar.handler === "string" ? ar.handler : "custom",
|
|
136
|
+
handler: typeof ar.handler === "string" ? ar.handler : void 0,
|
|
137
|
+
summary: ar.summary
|
|
138
|
+
})) ?? []
|
|
139
|
+
],
|
|
140
|
+
events: r.events
|
|
141
|
+
};
|
|
142
|
+
}),
|
|
143
|
+
stats: this.getStats(),
|
|
144
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Freeze registry (prevent further registrations)
|
|
149
|
+
*/
|
|
150
|
+
freeze() {
|
|
151
|
+
this._frozen = true;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Check if frozen
|
|
155
|
+
*/
|
|
156
|
+
isFrozen() {
|
|
157
|
+
return this._frozen;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Unfreeze registry (for testing)
|
|
161
|
+
*/
|
|
162
|
+
_unfreeze() {
|
|
163
|
+
this._frozen = false;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Clear all resources (for testing)
|
|
167
|
+
*/
|
|
168
|
+
_clear() {
|
|
169
|
+
this._resources.clear();
|
|
170
|
+
this._frozen = false;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Group by key
|
|
174
|
+
*/
|
|
175
|
+
_groupBy(arr, key) {
|
|
176
|
+
const result = {};
|
|
177
|
+
for (const item of arr) {
|
|
178
|
+
const k = String(item[key] ?? "uncategorized");
|
|
179
|
+
result[k] = (result[k] ?? 0) + 1;
|
|
180
|
+
}
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
var registryKey = /* @__PURE__ */ Symbol.for("arc.resourceRegistry");
|
|
185
|
+
var globalScope = globalThis;
|
|
186
|
+
var resourceRegistry = globalScope[registryKey] ?? new ResourceRegistry();
|
|
187
|
+
if (!globalScope[registryKey]) {
|
|
188
|
+
globalScope[registryKey] = resourceRegistry;
|
|
189
|
+
}
|
|
190
|
+
var introspectionPlugin = async (fastify, opts = {}) => {
|
|
191
|
+
const {
|
|
192
|
+
prefix = "/_resources",
|
|
193
|
+
authRoles = ["superadmin"],
|
|
194
|
+
enabled = process.env.NODE_ENV !== "production" || process.env.ENABLE_INTROSPECTION === "true"
|
|
195
|
+
} = opts;
|
|
196
|
+
if (!enabled) {
|
|
197
|
+
fastify.log?.info?.("Introspection plugin disabled");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const typedFastify = fastify;
|
|
201
|
+
const authMiddleware = authRoles.length > 0 && typedFastify.authenticate ? [
|
|
202
|
+
typedFastify.authenticate,
|
|
203
|
+
typedFastify.authorize?.(...authRoles)
|
|
204
|
+
].filter(Boolean) : [];
|
|
205
|
+
await fastify.register(async (instance) => {
|
|
206
|
+
instance.get(
|
|
207
|
+
"/",
|
|
208
|
+
{
|
|
209
|
+
preHandler: authMiddleware
|
|
210
|
+
},
|
|
211
|
+
async (_req, _reply) => {
|
|
212
|
+
return resourceRegistry.getIntrospection();
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
instance.get(
|
|
216
|
+
"/stats",
|
|
217
|
+
{
|
|
218
|
+
preHandler: authMiddleware
|
|
219
|
+
},
|
|
220
|
+
async (_req, _reply) => {
|
|
221
|
+
return resourceRegistry.getStats();
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
instance.get(
|
|
225
|
+
"/:name",
|
|
226
|
+
{
|
|
227
|
+
schema: {
|
|
228
|
+
params: {
|
|
229
|
+
type: "object",
|
|
230
|
+
properties: {
|
|
231
|
+
name: { type: "string" }
|
|
232
|
+
},
|
|
233
|
+
required: ["name"]
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
preHandler: authMiddleware
|
|
237
|
+
},
|
|
238
|
+
async (req, reply) => {
|
|
239
|
+
const resource = resourceRegistry.get(req.params.name);
|
|
240
|
+
if (!resource) {
|
|
241
|
+
return reply.code(404).send({
|
|
242
|
+
error: `Resource '${req.params.name}' not found`
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
return resource;
|
|
246
|
+
}
|
|
247
|
+
);
|
|
248
|
+
}, { prefix });
|
|
249
|
+
fastify.log?.info?.(`Introspection API at ${prefix}`);
|
|
250
|
+
};
|
|
251
|
+
var introspectionPlugin_default = fp(introspectionPlugin, { name: "arc-introspection" });
|
|
252
|
+
|
|
253
|
+
export { ResourceRegistry, introspectionPlugin_default as introspectionPlugin, introspectionPlugin as introspectionPluginFn, resourceRegistry };
|