@arkveil/cli 1.0.0 → 1.1.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/README.md +49 -0
- package/dist/index.js +737 -19
- package/dist/index.js.map +1 -1
- package/package.json +11 -13
package/dist/index.js
CHANGED
|
@@ -590,15 +590,15 @@ var require_help = __commonJS({
|
|
|
590
590
|
* @return {string}
|
|
591
591
|
*
|
|
592
592
|
*/
|
|
593
|
-
wrap(str, width,
|
|
593
|
+
wrap(str, width, indent2, minColumnWidth = 40) {
|
|
594
594
|
const indents = " \\f\\t\\v\xA0\u1680\u2000-\u200A\u202F\u205F\u3000\uFEFF";
|
|
595
595
|
const manualIndent = new RegExp(`[\\n][${indents}]+`);
|
|
596
596
|
if (str.match(manualIndent)) return str;
|
|
597
|
-
const columnWidth = width -
|
|
597
|
+
const columnWidth = width - indent2;
|
|
598
598
|
if (columnWidth < minColumnWidth) return str;
|
|
599
|
-
const leadingStr = str.slice(0,
|
|
600
|
-
const columnText = str.slice(
|
|
601
|
-
const indentString = " ".repeat(
|
|
599
|
+
const leadingStr = str.slice(0, indent2);
|
|
600
|
+
const columnText = str.slice(indent2).replace("\r\n", "\n");
|
|
601
|
+
const indentString = " ".repeat(indent2);
|
|
602
602
|
const zeroWidthSpace = "\u200B";
|
|
603
603
|
const breaks = `\\s${zeroWidthSpace}`;
|
|
604
604
|
const regex2 = new RegExp(
|
|
@@ -5276,14 +5276,14 @@ var init_open = __esm({
|
|
|
5276
5276
|
}
|
|
5277
5277
|
const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions);
|
|
5278
5278
|
if (options.wait) {
|
|
5279
|
-
return new Promise((
|
|
5279
|
+
return new Promise((resolve2, reject) => {
|
|
5280
5280
|
subprocess.once("error", reject);
|
|
5281
5281
|
subprocess.once("close", (exitCode) => {
|
|
5282
5282
|
if (!options.allowNonzeroExitCode && exitCode > 0) {
|
|
5283
5283
|
reject(new Error(`Exited with code ${exitCode}`));
|
|
5284
5284
|
return;
|
|
5285
5285
|
}
|
|
5286
|
-
|
|
5286
|
+
resolve2(subprocess);
|
|
5287
5287
|
});
|
|
5288
5288
|
});
|
|
5289
5289
|
}
|
|
@@ -10913,11 +10913,11 @@ var Ora = class {
|
|
|
10913
10913
|
get indent() {
|
|
10914
10914
|
return this.#indent;
|
|
10915
10915
|
}
|
|
10916
|
-
set indent(
|
|
10917
|
-
if (!(
|
|
10916
|
+
set indent(indent2 = 0) {
|
|
10917
|
+
if (!(indent2 >= 0 && Number.isInteger(indent2))) {
|
|
10918
10918
|
throw new Error("The `indent` option must be an integer from 0 and up");
|
|
10919
10919
|
}
|
|
10920
|
-
this.#indent =
|
|
10920
|
+
this.#indent = indent2;
|
|
10921
10921
|
this.#updateLineCount();
|
|
10922
10922
|
}
|
|
10923
10923
|
get interval() {
|
|
@@ -11750,7 +11750,7 @@ import { randomUUID } from "crypto";
|
|
|
11750
11750
|
var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
11751
11751
|
var RETRYABLE_STATUS = /* @__PURE__ */ new Set([429, 502, 503, 504]);
|
|
11752
11752
|
var MAX_BACKOFF_MS = 5e3;
|
|
11753
|
-
var sleep = (ms) => new Promise((
|
|
11753
|
+
var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
11754
11754
|
function backoffDelay(attempt, retryAfter) {
|
|
11755
11755
|
if (retryAfter) {
|
|
11756
11756
|
const seconds = Number(retryAfter);
|
|
@@ -12205,7 +12205,7 @@ function extractOAuthErrorDescription(json) {
|
|
|
12205
12205
|
}
|
|
12206
12206
|
return void 0;
|
|
12207
12207
|
}
|
|
12208
|
-
var sleep2 = (ms) => new Promise((
|
|
12208
|
+
var sleep2 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
12209
12209
|
async function parseJson(response, url) {
|
|
12210
12210
|
const text = await safeText(response);
|
|
12211
12211
|
if (!text) return {};
|
|
@@ -13256,7 +13256,10 @@ async function setSchema(ctx, type, options) {
|
|
|
13256
13256
|
var TYPE_CHOICES = ["user", "context", "action"];
|
|
13257
13257
|
function registerSchemas(program2) {
|
|
13258
13258
|
const schemas = program2.command("schemas").description("Manage USER/CONTEXT/ACTION attribute JSON schemas");
|
|
13259
|
-
schemas.command("get").description("Show the JSON Schema for an attribute type").addArgument(new Argument("<type>", "attribute schema type").choices(TYPE_CHOICES)).
|
|
13259
|
+
schemas.command("get").description("Show the JSON Schema for an attribute type").addArgument(new Argument("<type>", "attribute schema type").choices(TYPE_CHOICES)).addHelpText(
|
|
13260
|
+
"after",
|
|
13261
|
+
"\nTo type the SDK from these schemas (user/context), see the recipe in\n`arkveil sdk info` \u2014 fetch with --json, generate TypeScript, and augment\nthe SDK's ArkveilUserRegistry / ArkveilContextRegistry.\n"
|
|
13262
|
+
).action(async (type, _options, command) => {
|
|
13260
13263
|
await run(command, (ctx) => getSchema(ctx, type));
|
|
13261
13264
|
});
|
|
13262
13265
|
schemas.command("set").description("Replace the JSON Schema for an attribute type").addArgument(new Argument("<type>", "attribute schema type").choices(TYPE_CHOICES)).option("--data <json>", "JSON Schema: inline JSON, @file, or - for stdin").addHelpText("after", "\nExample:\n $ arkveil schemas set user --data @user-schema.json\n").action(async (type, options, command) => {
|
|
@@ -13264,6 +13267,614 @@ function registerSchemas(program2) {
|
|
|
13264
13267
|
});
|
|
13265
13268
|
}
|
|
13266
13269
|
|
|
13270
|
+
// src/commands/sdk/catalog.ts
|
|
13271
|
+
var SDK_CATALOG = {
|
|
13272
|
+
language: "TypeScript / JavaScript",
|
|
13273
|
+
languages: ["TypeScript", "JavaScript"],
|
|
13274
|
+
registry: "npm",
|
|
13275
|
+
requirements: { node: ">=18", typescript: "^5 (for typed usage)" },
|
|
13276
|
+
note: "Only TypeScript / JavaScript is supported today, via three packages: the core SDK (arkveil), Node.js/Express (@arkveil/node), and NestJS (@arkveil/nest). There are no SDKs for other languages yet.",
|
|
13277
|
+
targets: [
|
|
13278
|
+
{
|
|
13279
|
+
id: "nest",
|
|
13280
|
+
title: "NestJS SDK",
|
|
13281
|
+
package: "@arkveil/nest",
|
|
13282
|
+
platform: "NestJS (Node.js)",
|
|
13283
|
+
frameworks: ["NestJS"],
|
|
13284
|
+
whenToUse: "Use in a NestJS application. Configure once with ArkveilModule.forRoot and protect routes declaratively with the @PermissionPoint decorator.",
|
|
13285
|
+
install: "npm install @arkveil/nest",
|
|
13286
|
+
quickStart: `import { Module } from "@nestjs/common";
|
|
13287
|
+
import { ArkveilModule } from "@arkveil/nest";
|
|
13288
|
+
|
|
13289
|
+
@Module({
|
|
13290
|
+
imports: [
|
|
13291
|
+
ArkveilModule.forRoot({
|
|
13292
|
+
serviceUrl: "https://api.arkveil.com",
|
|
13293
|
+
apiKey: process.env.ARKVEIL_API_KEY!,
|
|
13294
|
+
getUserAttributes: (req) => ({ id: req.user?.id, role: req.user?.role }),
|
|
13295
|
+
}),
|
|
13296
|
+
],
|
|
13297
|
+
})
|
|
13298
|
+
export class AppModule {}
|
|
13299
|
+
|
|
13300
|
+
// Protect a route \u2014 the code is type-checked once the registry is augmented:
|
|
13301
|
+
import { Controller, Delete } from "@nestjs/common";
|
|
13302
|
+
import { PermissionPoint } from "@arkveil/nest";
|
|
13303
|
+
|
|
13304
|
+
@Controller("articles")
|
|
13305
|
+
export class ArticlesController {
|
|
13306
|
+
@Delete(":id")
|
|
13307
|
+
@PermissionPoint("content-service.article-delete")
|
|
13308
|
+
remove() {
|
|
13309
|
+
return "Protected content";
|
|
13310
|
+
}
|
|
13311
|
+
}`,
|
|
13312
|
+
docs: "https://www.npmjs.com/package/@arkveil/nest"
|
|
13313
|
+
},
|
|
13314
|
+
{
|
|
13315
|
+
id: "node",
|
|
13316
|
+
title: "Node.js / Express SDK",
|
|
13317
|
+
package: "@arkveil/node",
|
|
13318
|
+
platform: "Node.js (Express, Fastify, and other HTTP frameworks)",
|
|
13319
|
+
frameworks: ["Express", "Fastify", "Node.js HTTP"],
|
|
13320
|
+
whenToUse: "Use in a non-Nest Node.js HTTP server. Provides a `permissionPoint(code)` middleware for Express/Fastify-style apps.",
|
|
13321
|
+
install: "npm install @arkveil/node arkveil",
|
|
13322
|
+
quickStart: `import { Arkveil } from "@arkveil/node";
|
|
13323
|
+
|
|
13324
|
+
const arkveil = new Arkveil({
|
|
13325
|
+
serviceUrl: "https://api.arkveil.com",
|
|
13326
|
+
apiKey: process.env.ARKVEIL_API_KEY!,
|
|
13327
|
+
getUserAttributes: (req) => ({ id: req.user?.id, role: req.user?.role }),
|
|
13328
|
+
onDenied: (req, res) => res.status(403).json({ error: "Forbidden" }),
|
|
13329
|
+
});
|
|
13330
|
+
|
|
13331
|
+
app.post(
|
|
13332
|
+
"/api/admin",
|
|
13333
|
+
arkveil.permissionPoint("content-service.article-delete"),
|
|
13334
|
+
(req, res) => res.json({ message: "Protected content" }),
|
|
13335
|
+
);`,
|
|
13336
|
+
docs: "https://www.npmjs.com/package/@arkveil/node"
|
|
13337
|
+
},
|
|
13338
|
+
{
|
|
13339
|
+
id: "core",
|
|
13340
|
+
title: "Core SDK (runtime-agnostic)",
|
|
13341
|
+
package: "arkveil",
|
|
13342
|
+
platform: "Any JavaScript runtime (Node.js, edge, workers, browser)",
|
|
13343
|
+
frameworks: ["any"],
|
|
13344
|
+
whenToUse: "Use when you want to call Arkveil directly (no middleware), or to build your own platform integration. @arkveil/node and @arkveil/nest are built on top of this.",
|
|
13345
|
+
install: "npm install arkveil",
|
|
13346
|
+
quickStart: `import { Arkveil } from "arkveil";
|
|
13347
|
+
|
|
13348
|
+
const arkveil = new Arkveil({
|
|
13349
|
+
serviceUrl: "https://api.arkveil.com",
|
|
13350
|
+
apiKey: process.env.ARKVEIL_API_KEY!,
|
|
13351
|
+
});
|
|
13352
|
+
|
|
13353
|
+
const { granted } = await arkveil.checkPermission({
|
|
13354
|
+
code: "content-service.article-delete",
|
|
13355
|
+
user: { id: "user-123", role: "admin" },
|
|
13356
|
+
context: {},
|
|
13357
|
+
});
|
|
13358
|
+
|
|
13359
|
+
if (granted) {
|
|
13360
|
+
// allow
|
|
13361
|
+
} else {
|
|
13362
|
+
// deny
|
|
13363
|
+
}`,
|
|
13364
|
+
docs: "https://www.npmjs.com/package/arkveil"
|
|
13365
|
+
}
|
|
13366
|
+
],
|
|
13367
|
+
typing: {
|
|
13368
|
+
summary: "The SDK is typed by declaration merging. `arkveil generate typescript` reads this project's permission codes and user/context JSON Schemas and writes one TypeScript file that augments the SDK registries. Until you import it, attributes are Record<string, any> and codes are string, so untyped usage keeps working.",
|
|
13369
|
+
command: "arkveil generate typescript -o src/arkveil.generated.ts",
|
|
13370
|
+
registries: [
|
|
13371
|
+
{
|
|
13372
|
+
interface: "ArkveilUserRegistry",
|
|
13373
|
+
key: "attributes",
|
|
13374
|
+
resolves: "ArkveilUser",
|
|
13375
|
+
source: "arkveil schemas get user --json"
|
|
13376
|
+
},
|
|
13377
|
+
{
|
|
13378
|
+
interface: "ArkveilContextRegistry",
|
|
13379
|
+
key: "attributes",
|
|
13380
|
+
resolves: "ArkveilContext",
|
|
13381
|
+
source: "arkveil schemas get context --json"
|
|
13382
|
+
},
|
|
13383
|
+
{
|
|
13384
|
+
interface: "ArkveilCodeRegistry",
|
|
13385
|
+
key: "codes",
|
|
13386
|
+
resolves: "ArkveilCode",
|
|
13387
|
+
source: "the permission/action codes defined in your project"
|
|
13388
|
+
}
|
|
13389
|
+
],
|
|
13390
|
+
steps: [
|
|
13391
|
+
"Run `arkveil generate typescript -o src/arkveil.generated.ts` \u2014 it fetches the codes + attribute schemas and writes the typed file for you (use `--include user,context` to skip codes).",
|
|
13392
|
+
'Import the generated file once as a side-effect import (`import "./arkveil.generated"`) so the `declare module "arkveil"` augmentation is in scope.',
|
|
13393
|
+
"Permission codes plus `getUserAttributes`, `getContextAttributes`, and `checkPermission` are now type-checked against this project.",
|
|
13394
|
+
"Re-run the command whenever the project's codes or attribute schemas change to keep the types in sync.",
|
|
13395
|
+
"Manual alternative: `arkveil schemas get user|context --json` returns the raw JSON Schema (under `.jsonSchema`) if you prefer to generate the types yourself."
|
|
13396
|
+
],
|
|
13397
|
+
example: `// arkveil-attributes.generated.ts \u2014 generated from \`arkveil schemas get\`
|
|
13398
|
+
export interface ArkveilUserAttributes {
|
|
13399
|
+
id?: string;
|
|
13400
|
+
role: "admin" | "editor" | "viewer";
|
|
13401
|
+
}
|
|
13402
|
+
|
|
13403
|
+
export interface ArkveilContextAttributes {
|
|
13404
|
+
ipAddress?: string;
|
|
13405
|
+
region?: "EU" | "US";
|
|
13406
|
+
}
|
|
13407
|
+
|
|
13408
|
+
declare module "arkveil" {
|
|
13409
|
+
interface ArkveilUserRegistry {
|
|
13410
|
+
attributes: ArkveilUserAttributes;
|
|
13411
|
+
}
|
|
13412
|
+
interface ArkveilContextRegistry {
|
|
13413
|
+
attributes: ArkveilContextAttributes;
|
|
13414
|
+
}
|
|
13415
|
+
}`
|
|
13416
|
+
}
|
|
13417
|
+
};
|
|
13418
|
+
function findTarget(id) {
|
|
13419
|
+
return SDK_CATALOG.targets.find((t) => t.id === id);
|
|
13420
|
+
}
|
|
13421
|
+
var SDK_TARGET_IDS = SDK_CATALOG.targets.map((t) => t.id);
|
|
13422
|
+
function renderTarget(t) {
|
|
13423
|
+
return [
|
|
13424
|
+
`${t.title} (${t.package})`,
|
|
13425
|
+
` Platform: ${t.platform}`,
|
|
13426
|
+
` Frameworks: ${t.frameworks.join(", ")}`,
|
|
13427
|
+
` When: ${t.whenToUse}`,
|
|
13428
|
+
` Install: ${t.install}`,
|
|
13429
|
+
"",
|
|
13430
|
+
indent(t.quickStart, 2)
|
|
13431
|
+
].join("\n");
|
|
13432
|
+
}
|
|
13433
|
+
function renderTyping(typing) {
|
|
13434
|
+
const registries = typing.registries.map(
|
|
13435
|
+
(r2) => ` \u2022 ${r2.interface} (key "${r2.key}") \u2192 ${r2.resolves}
|
|
13436
|
+
from: ${r2.source}`
|
|
13437
|
+
).join("\n");
|
|
13438
|
+
const steps = typing.steps.map((s, i) => ` ${i + 1}. ${s}`).join("\n");
|
|
13439
|
+
return [
|
|
13440
|
+
"TYPED CODES, USER & CONTEXT ATTRIBUTES",
|
|
13441
|
+
` ${typing.summary}`,
|
|
13442
|
+
"",
|
|
13443
|
+
` Generate it: ${typing.command}`,
|
|
13444
|
+
"",
|
|
13445
|
+
" Registries to augment:",
|
|
13446
|
+
registries,
|
|
13447
|
+
"",
|
|
13448
|
+
" Steps:",
|
|
13449
|
+
steps,
|
|
13450
|
+
"",
|
|
13451
|
+
" Example generated file:",
|
|
13452
|
+
indent(typing.example, 4)
|
|
13453
|
+
].join("\n");
|
|
13454
|
+
}
|
|
13455
|
+
function renderCatalog(target) {
|
|
13456
|
+
const c = SDK_CATALOG;
|
|
13457
|
+
const header = [
|
|
13458
|
+
"Arkveil SDK \u2014 install & usage",
|
|
13459
|
+
"",
|
|
13460
|
+
`Language: ${c.language}`,
|
|
13461
|
+
`Registry: ${c.registry}`,
|
|
13462
|
+
`Requirements: Node ${c.requirements.node}, TypeScript ${c.requirements.typescript}`,
|
|
13463
|
+
"",
|
|
13464
|
+
c.note
|
|
13465
|
+
].join("\n");
|
|
13466
|
+
if (target) {
|
|
13467
|
+
return [header, "", renderTarget(target)].join("\n");
|
|
13468
|
+
}
|
|
13469
|
+
const targets = c.targets.map(renderTarget).join("\n\n");
|
|
13470
|
+
return [
|
|
13471
|
+
header,
|
|
13472
|
+
"",
|
|
13473
|
+
"TARGETS",
|
|
13474
|
+
"",
|
|
13475
|
+
targets,
|
|
13476
|
+
"",
|
|
13477
|
+
renderTyping(c.typing),
|
|
13478
|
+
"",
|
|
13479
|
+
"See also: `arkveil sdk install <core|node|nest>`, `arkveil schemas get <user|context>`."
|
|
13480
|
+
].join("\n");
|
|
13481
|
+
}
|
|
13482
|
+
function indent(text, spaces) {
|
|
13483
|
+
const pad = " ".repeat(spaces);
|
|
13484
|
+
return text.split("\n").map((line) => line.length > 0 ? pad + line : line).join("\n");
|
|
13485
|
+
}
|
|
13486
|
+
|
|
13487
|
+
// src/commands/sdk/info.ts
|
|
13488
|
+
function sdkInfo(ctx, target) {
|
|
13489
|
+
if (target !== void 0 && !findTarget(target)) {
|
|
13490
|
+
throw new UsageError(
|
|
13491
|
+
`Unknown SDK target "${target}".`,
|
|
13492
|
+
`Choose one of: ${SDK_TARGET_IDS.join(", ")}.`
|
|
13493
|
+
);
|
|
13494
|
+
}
|
|
13495
|
+
const selected = target ? findTarget(target) : void 0;
|
|
13496
|
+
const jsonValue = selected ? { ...SDK_CATALOG, targets: [selected] } : SDK_CATALOG;
|
|
13497
|
+
ctx.out.data(jsonValue, () => renderCatalog(selected));
|
|
13498
|
+
return Promise.resolve();
|
|
13499
|
+
}
|
|
13500
|
+
|
|
13501
|
+
// src/commands/sdk/install.ts
|
|
13502
|
+
function sdkInstall(ctx, target) {
|
|
13503
|
+
const t = findTarget(target);
|
|
13504
|
+
if (!t) {
|
|
13505
|
+
throw new UsageError(
|
|
13506
|
+
`Unknown SDK target "${target}".`,
|
|
13507
|
+
`Choose one of: ${SDK_TARGET_IDS.join(", ")}.`
|
|
13508
|
+
);
|
|
13509
|
+
}
|
|
13510
|
+
ctx.out.data(
|
|
13511
|
+
{ id: t.id, package: t.package, install: t.install, registry: SDK_CATALOG.registry },
|
|
13512
|
+
() => t.install
|
|
13513
|
+
);
|
|
13514
|
+
return Promise.resolve();
|
|
13515
|
+
}
|
|
13516
|
+
|
|
13517
|
+
// src/commands/sdk/index.ts
|
|
13518
|
+
var TARGET_CHOICES = SDK_TARGET_IDS;
|
|
13519
|
+
function registerSdk(program2) {
|
|
13520
|
+
const sdk = program2.command("sdk").description("How to install and use the Arkveil SDK (for humans and AI agents)");
|
|
13521
|
+
sdk.command("info").description("Print SDK install + usage info (all targets, or one)").addArgument(
|
|
13522
|
+
new Argument("[target]", "limit to one SDK target").choices(TARGET_CHOICES)
|
|
13523
|
+
).addHelpText(
|
|
13524
|
+
"after",
|
|
13525
|
+
`
|
|
13526
|
+
Targets:
|
|
13527
|
+
nest NestJS app (@arkveil/nest)
|
|
13528
|
+
node Node.js / Express (@arkveil/node)
|
|
13529
|
+
core runtime-agnostic (arkveil)
|
|
13530
|
+
|
|
13531
|
+
For AI agents: \`arkveil sdk info --json\` emits the full machine-readable
|
|
13532
|
+
catalog \u2014 packages, install commands, usage snippets, and the recipe to type
|
|
13533
|
+
the SDK from your project's attribute schemas (\`arkveil schemas get user|context\`).
|
|
13534
|
+
|
|
13535
|
+
Examples:
|
|
13536
|
+
$ arkveil sdk info # everything
|
|
13537
|
+
$ arkveil sdk info nest # just the NestJS package
|
|
13538
|
+
$ arkveil sdk info --json | jq . # structured output for tooling/agents
|
|
13539
|
+
`
|
|
13540
|
+
).action(async (target, _options, command) => {
|
|
13541
|
+
await run(command, (ctx) => sdkInfo(ctx, target));
|
|
13542
|
+
});
|
|
13543
|
+
sdk.command("install").description("Print the npm install command for an SDK target").addArgument(
|
|
13544
|
+
new Argument("<target>", "SDK target to install").choices(TARGET_CHOICES)
|
|
13545
|
+
).addHelpText(
|
|
13546
|
+
"after",
|
|
13547
|
+
'\nExample:\n $ arkveil sdk install nest\n $ eval "$(arkveil sdk install node)"\n'
|
|
13548
|
+
).action(async (target, _options, command) => {
|
|
13549
|
+
await run(command, (ctx) => sdkInstall(ctx, target));
|
|
13550
|
+
});
|
|
13551
|
+
}
|
|
13552
|
+
|
|
13553
|
+
// src/commands/generate/typescript.ts
|
|
13554
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
13555
|
+
import { resolve } from "path";
|
|
13556
|
+
|
|
13557
|
+
// src/lib/json-schema-to-ts.ts
|
|
13558
|
+
var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
13559
|
+
function tsLiteral(value) {
|
|
13560
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
13561
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
13562
|
+
if (value === null) return "null";
|
|
13563
|
+
return JSON.stringify(value);
|
|
13564
|
+
}
|
|
13565
|
+
function dedupe(values) {
|
|
13566
|
+
return [...new Set(values)];
|
|
13567
|
+
}
|
|
13568
|
+
function commentText(text) {
|
|
13569
|
+
return text.replace(/\*\//g, "*\\/").replace(/\s+/g, " ").trim();
|
|
13570
|
+
}
|
|
13571
|
+
function indentOf(spaces) {
|
|
13572
|
+
return " ".repeat(spaces);
|
|
13573
|
+
}
|
|
13574
|
+
function resolveRef(ref, ctx) {
|
|
13575
|
+
const local = ref.match(/^#\/(\$defs|definitions)\/(.+)$/);
|
|
13576
|
+
if (!local) return void 0;
|
|
13577
|
+
const bag = local[1] === "$defs" ? ctx.root.$defs : ctx.root.definitions;
|
|
13578
|
+
const key = decodeURIComponent(local[2]);
|
|
13579
|
+
return bag?.[key];
|
|
13580
|
+
}
|
|
13581
|
+
function needsParensForArray(t) {
|
|
13582
|
+
return t.includes("|") || t.includes("&");
|
|
13583
|
+
}
|
|
13584
|
+
function arrayType2(schema, indent2, ctx) {
|
|
13585
|
+
if (Array.isArray(schema.items)) {
|
|
13586
|
+
const tuple = schema.items.map((s) => schemaToType(s, indent2, ctx)).join(", ");
|
|
13587
|
+
return `[${tuple}]`;
|
|
13588
|
+
}
|
|
13589
|
+
const item = schema.items ? schemaToType(schema.items, indent2, ctx) : "unknown";
|
|
13590
|
+
return needsParensForArray(item) ? `(${item})[]` : `${item}[]`;
|
|
13591
|
+
}
|
|
13592
|
+
function objectType2(schema, indent2, ctx) {
|
|
13593
|
+
const props = schema.properties ?? {};
|
|
13594
|
+
const keys = Object.keys(props);
|
|
13595
|
+
const required = new Set(schema.required ?? []);
|
|
13596
|
+
const inner = indent2 + 2;
|
|
13597
|
+
const lines = [];
|
|
13598
|
+
for (const key of keys) {
|
|
13599
|
+
const propSchema = props[key];
|
|
13600
|
+
const optional = required.has(key) ? "" : "?";
|
|
13601
|
+
const safeKey = IDENTIFIER.test(key) ? key : JSON.stringify(key);
|
|
13602
|
+
if (typeof propSchema.description === "string" && propSchema.description.trim()) {
|
|
13603
|
+
lines.push(`${indentOf(inner)}/** ${commentText(propSchema.description)} */`);
|
|
13604
|
+
}
|
|
13605
|
+
lines.push(
|
|
13606
|
+
`${indentOf(inner)}${safeKey}${optional}: ${schemaToType(propSchema, inner, ctx)};`
|
|
13607
|
+
);
|
|
13608
|
+
}
|
|
13609
|
+
const ap = schema.additionalProperties;
|
|
13610
|
+
if (ap && ap !== true) {
|
|
13611
|
+
lines.push(`${indentOf(inner)}[key: string]: ${schemaToType(ap, inner, ctx)};`);
|
|
13612
|
+
} else if (ap === true) {
|
|
13613
|
+
lines.push(`${indentOf(inner)}[key: string]: unknown;`);
|
|
13614
|
+
}
|
|
13615
|
+
if (lines.length === 0) {
|
|
13616
|
+
return ap === false ? "Record<string, never>" : "Record<string, unknown>";
|
|
13617
|
+
}
|
|
13618
|
+
return `{
|
|
13619
|
+
${lines.join("\n")}
|
|
13620
|
+
${indentOf(indent2)}}`;
|
|
13621
|
+
}
|
|
13622
|
+
function unionOf(schemas, indent2, ctx) {
|
|
13623
|
+
const parts = dedupe(schemas.map((s) => schemaToType(s, indent2, ctx)));
|
|
13624
|
+
return parts.length > 0 ? parts.join(" | ") : "unknown";
|
|
13625
|
+
}
|
|
13626
|
+
function schemaToType(schema, indent2 = 0, ctx = {
|
|
13627
|
+
root: typeof schema === "object" && schema ? schema : {},
|
|
13628
|
+
seen: /* @__PURE__ */ new Set()
|
|
13629
|
+
}) {
|
|
13630
|
+
if (schema === void 0 || schema === true) return "unknown";
|
|
13631
|
+
if (schema === false) return "never";
|
|
13632
|
+
if (typeof schema.$ref === "string") {
|
|
13633
|
+
if (ctx.seen.has(schema.$ref)) return "unknown";
|
|
13634
|
+
const resolved = resolveRef(schema.$ref, ctx);
|
|
13635
|
+
if (!resolved) return "unknown";
|
|
13636
|
+
ctx.seen.add(schema.$ref);
|
|
13637
|
+
const t = schemaToType(resolved, indent2, ctx);
|
|
13638
|
+
ctx.seen.delete(schema.$ref);
|
|
13639
|
+
return t;
|
|
13640
|
+
}
|
|
13641
|
+
if (schema.const !== void 0) return tsLiteral(schema.const);
|
|
13642
|
+
if (Array.isArray(schema.enum)) {
|
|
13643
|
+
const union = dedupe(schema.enum.map(tsLiteral));
|
|
13644
|
+
return union.length > 0 ? union.join(" | ") : "never";
|
|
13645
|
+
}
|
|
13646
|
+
if (Array.isArray(schema.anyOf)) return unionOf(schema.anyOf, indent2, ctx);
|
|
13647
|
+
if (Array.isArray(schema.oneOf)) return unionOf(schema.oneOf, indent2, ctx);
|
|
13648
|
+
if (Array.isArray(schema.allOf)) {
|
|
13649
|
+
const parts = dedupe(schema.allOf.map((s) => schemaToType(s, indent2, ctx)));
|
|
13650
|
+
return parts.length > 0 ? parts.join(" & ") : "unknown";
|
|
13651
|
+
}
|
|
13652
|
+
const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
|
|
13653
|
+
const withNullable = (t) => schema.nullable && !t.split("|").map((s) => s.trim()).includes("null") ? `${t} | null` : t;
|
|
13654
|
+
if (types.length === 0) return withNullable("unknown");
|
|
13655
|
+
if (types.length > 1) {
|
|
13656
|
+
const parts = dedupe(
|
|
13657
|
+
types.map(
|
|
13658
|
+
(t) => t === "null" ? "null" : schemaToType({ ...schema, type: t }, indent2, ctx)
|
|
13659
|
+
)
|
|
13660
|
+
);
|
|
13661
|
+
return parts.join(" | ");
|
|
13662
|
+
}
|
|
13663
|
+
switch (types[0]) {
|
|
13664
|
+
case "string":
|
|
13665
|
+
return withNullable("string");
|
|
13666
|
+
case "integer":
|
|
13667
|
+
case "number":
|
|
13668
|
+
return withNullable("number");
|
|
13669
|
+
case "boolean":
|
|
13670
|
+
return withNullable("boolean");
|
|
13671
|
+
case "null":
|
|
13672
|
+
return "null";
|
|
13673
|
+
case "array":
|
|
13674
|
+
return withNullable(arrayType2(schema, indent2, ctx));
|
|
13675
|
+
case "object":
|
|
13676
|
+
return withNullable(objectType2(schema, indent2, ctx));
|
|
13677
|
+
default:
|
|
13678
|
+
return withNullable("unknown");
|
|
13679
|
+
}
|
|
13680
|
+
}
|
|
13681
|
+
function schemaToNamedType(name, schema, description) {
|
|
13682
|
+
const ctx = { root: schema, seen: /* @__PURE__ */ new Set() };
|
|
13683
|
+
const body = schemaToType(schema, 0, ctx);
|
|
13684
|
+
const doc = description && description.trim() ? `/** ${commentText(description)} */
|
|
13685
|
+
` : "";
|
|
13686
|
+
if (body.startsWith("{")) {
|
|
13687
|
+
return `${doc}export interface ${name} ${body}`;
|
|
13688
|
+
}
|
|
13689
|
+
return `${doc}export type ${name} = ${body};`;
|
|
13690
|
+
}
|
|
13691
|
+
|
|
13692
|
+
// src/commands/generate/assemble.ts
|
|
13693
|
+
var TYPE_NAMES = {
|
|
13694
|
+
codes: "ArkveilCodes",
|
|
13695
|
+
user: "ArkveilUserAttributes",
|
|
13696
|
+
context: "ArkveilContextAttributes"
|
|
13697
|
+
};
|
|
13698
|
+
var HEADER = `// AUTO-GENERATED by \`arkveil generate typescript\`. Do not edit by hand.
|
|
13699
|
+
//
|
|
13700
|
+
// This file types the Arkveil SDK for TypeScript by declaration-merging into
|
|
13701
|
+
// the \`arkveil\` package. Import it once (a side-effect import is enough) and
|
|
13702
|
+
// permission codes plus \`user\` / \`context\` attributes become typed across
|
|
13703
|
+
// \`checkPermission\`, the Node \`permissionPoint\` middleware, and the NestJS
|
|
13704
|
+
// \`@PermissionPoint\` decorator. Regenerate whenever codes or schemas change.`;
|
|
13705
|
+
function renderCodes(codes) {
|
|
13706
|
+
if (codes.length === 0) {
|
|
13707
|
+
return `/** No permission codes were found in this project (falls back to \`string\`). */
|
|
13708
|
+
export type ${TYPE_NAMES.codes} = string;`;
|
|
13709
|
+
}
|
|
13710
|
+
const union = codes.map((c) => ` | ${JSON.stringify(c)}`).join("\n");
|
|
13711
|
+
return `/** Union of every permission code defined in this Arkveil project. */
|
|
13712
|
+
export type ${TYPE_NAMES.codes} =
|
|
13713
|
+
${union};`;
|
|
13714
|
+
}
|
|
13715
|
+
function assembleTypeScript(input) {
|
|
13716
|
+
const blocks = [HEADER];
|
|
13717
|
+
const augment = [];
|
|
13718
|
+
if (input.codes !== void 0) {
|
|
13719
|
+
blocks.push(renderCodes(input.codes));
|
|
13720
|
+
augment.push(` interface ArkveilCodeRegistry {
|
|
13721
|
+
codes: ${TYPE_NAMES.codes};
|
|
13722
|
+
}`);
|
|
13723
|
+
}
|
|
13724
|
+
if (input.userSchema !== void 0) {
|
|
13725
|
+
blocks.push(
|
|
13726
|
+
schemaToNamedType(
|
|
13727
|
+
TYPE_NAMES.user,
|
|
13728
|
+
input.userSchema,
|
|
13729
|
+
"Shape of `user` attributes (from `arkveil schemas get user`)."
|
|
13730
|
+
)
|
|
13731
|
+
);
|
|
13732
|
+
augment.push(
|
|
13733
|
+
` interface ArkveilUserRegistry {
|
|
13734
|
+
attributes: ${TYPE_NAMES.user};
|
|
13735
|
+
}`
|
|
13736
|
+
);
|
|
13737
|
+
}
|
|
13738
|
+
if (input.contextSchema !== void 0) {
|
|
13739
|
+
blocks.push(
|
|
13740
|
+
schemaToNamedType(
|
|
13741
|
+
TYPE_NAMES.context,
|
|
13742
|
+
input.contextSchema,
|
|
13743
|
+
"Shape of `context` attributes (from `arkveil schemas get context`)."
|
|
13744
|
+
)
|
|
13745
|
+
);
|
|
13746
|
+
augment.push(
|
|
13747
|
+
` interface ArkveilContextRegistry {
|
|
13748
|
+
attributes: ${TYPE_NAMES.context};
|
|
13749
|
+
}`
|
|
13750
|
+
);
|
|
13751
|
+
}
|
|
13752
|
+
blocks.push(
|
|
13753
|
+
`// Register the generated types globally so the default \`Arkveil\` generics
|
|
13754
|
+
// pick them up automatically \u2014 no need to pass them everywhere.
|
|
13755
|
+
declare module "arkveil" {
|
|
13756
|
+
${augment.join("\n")}
|
|
13757
|
+
}`
|
|
13758
|
+
);
|
|
13759
|
+
return `${blocks.join("\n\n")}
|
|
13760
|
+
`;
|
|
13761
|
+
}
|
|
13762
|
+
|
|
13763
|
+
// src/commands/generate/typescript.ts
|
|
13764
|
+
var ALL_INCLUDES = ["codes", "user", "context"];
|
|
13765
|
+
function parseInclude(value) {
|
|
13766
|
+
if (value === void 0) return ALL_INCLUDES;
|
|
13767
|
+
const items = value.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
13768
|
+
if (items.length === 0) {
|
|
13769
|
+
throw new UsageError(
|
|
13770
|
+
"--include must list at least one of: codes, user, context.",
|
|
13771
|
+
"Example: --include user,context"
|
|
13772
|
+
);
|
|
13773
|
+
}
|
|
13774
|
+
const invalid = items.filter((i) => !ALL_INCLUDES.includes(i));
|
|
13775
|
+
if (invalid.length > 0) {
|
|
13776
|
+
throw new UsageError(
|
|
13777
|
+
`Unknown --include value(s): ${invalid.join(", ")}.`,
|
|
13778
|
+
"Allowed values are: codes, user, context."
|
|
13779
|
+
);
|
|
13780
|
+
}
|
|
13781
|
+
return ALL_INCLUDES.filter((i) => items.includes(i));
|
|
13782
|
+
}
|
|
13783
|
+
function collectActionCodes(node, into) {
|
|
13784
|
+
if (node.resourceType === "ACTION" && node.resource && "code" in node.resource) {
|
|
13785
|
+
const code = node.resource.code;
|
|
13786
|
+
if (typeof code === "string" && code.length > 0) into.add(code);
|
|
13787
|
+
}
|
|
13788
|
+
for (const child of node.children ?? []) {
|
|
13789
|
+
collectActionCodes(child, into);
|
|
13790
|
+
}
|
|
13791
|
+
}
|
|
13792
|
+
async function fetchSchema(ctx, type) {
|
|
13793
|
+
const client = await ctx.getClient({ requireAuth: true });
|
|
13794
|
+
const res = await unwrap(
|
|
13795
|
+
client.GET("/api/v1/attribute-schemas/{type}", { params: { path: { type } } }),
|
|
13796
|
+
"GET"
|
|
13797
|
+
);
|
|
13798
|
+
return res.jsonSchema ?? {};
|
|
13799
|
+
}
|
|
13800
|
+
async function fetchActionCodes(ctx) {
|
|
13801
|
+
const client = await ctx.getClient({ requireAuth: true });
|
|
13802
|
+
const tree = await unwrap(
|
|
13803
|
+
client.GET("/api/v1/navigation/trees/actions"),
|
|
13804
|
+
"GET"
|
|
13805
|
+
);
|
|
13806
|
+
const codes = /* @__PURE__ */ new Set();
|
|
13807
|
+
if (tree?.root) collectActionCodes(tree.root, codes);
|
|
13808
|
+
return [...codes].sort();
|
|
13809
|
+
}
|
|
13810
|
+
async function generateTypeScript(ctx, options) {
|
|
13811
|
+
const include = parseInclude(options.include);
|
|
13812
|
+
const spinner = ctx.out.spinner("Generating TypeScript\u2026");
|
|
13813
|
+
const input = {};
|
|
13814
|
+
let codes = [];
|
|
13815
|
+
try {
|
|
13816
|
+
if (include.includes("codes")) {
|
|
13817
|
+
codes = await fetchActionCodes(ctx);
|
|
13818
|
+
input.codes = codes;
|
|
13819
|
+
}
|
|
13820
|
+
if (include.includes("user")) {
|
|
13821
|
+
input.userSchema = await fetchSchema(ctx, "user");
|
|
13822
|
+
}
|
|
13823
|
+
if (include.includes("context")) {
|
|
13824
|
+
input.contextSchema = await fetchSchema(ctx, "context");
|
|
13825
|
+
}
|
|
13826
|
+
spinner.stop();
|
|
13827
|
+
} catch (err) {
|
|
13828
|
+
spinner.fail("Could not generate TypeScript.");
|
|
13829
|
+
throw err;
|
|
13830
|
+
}
|
|
13831
|
+
const code = assembleTypeScript(input);
|
|
13832
|
+
if (include.includes("codes") && codes.length === 0) {
|
|
13833
|
+
ctx.out.warn(
|
|
13834
|
+
"No permission codes found; ArkveilCodes falls back to `string`. Define actions first."
|
|
13835
|
+
);
|
|
13836
|
+
}
|
|
13837
|
+
if (options.output) {
|
|
13838
|
+
const target = resolve(process.cwd(), options.output);
|
|
13839
|
+
writeFileSync2(target, code, "utf8");
|
|
13840
|
+
ctx.out.success(`Wrote ${include.join(", ")} TypeScript to ${target}`);
|
|
13841
|
+
ctx.out.data({ output: target, include, codeCount: codes.length }, () => void 0);
|
|
13842
|
+
return;
|
|
13843
|
+
}
|
|
13844
|
+
ctx.out.data(
|
|
13845
|
+
{ language: "typescript", include, codeCount: codes.length, code },
|
|
13846
|
+
() => code
|
|
13847
|
+
);
|
|
13848
|
+
}
|
|
13849
|
+
|
|
13850
|
+
// src/commands/generate/index.ts
|
|
13851
|
+
function registerGenerate(program2) {
|
|
13852
|
+
const generate = program2.command("generate").alias("gen").description("Generate typed SDK code from this project (TypeScript)");
|
|
13853
|
+
generate.command("typescript").alias("ts").description("Generate a TypeScript file that types the Arkveil SDK").option(
|
|
13854
|
+
"--include <items>",
|
|
13855
|
+
"comma-separated subset of codes,user,context",
|
|
13856
|
+
"codes,user,context"
|
|
13857
|
+
).option("-o, --output <file>", "write to a file instead of stdout").addHelpText(
|
|
13858
|
+
"after",
|
|
13859
|
+
`
|
|
13860
|
+
Generates TypeScript that types the Arkveil SDK by declaration-merging into the
|
|
13861
|
+
\`arkveil\` package: a permission-code union (ArkveilCodes) and \`user\` /
|
|
13862
|
+
\`context\` attribute types (ArkveilUserAttributes / ArkveilContextAttributes),
|
|
13863
|
+
sourced from this project's actions and attribute schemas. Import the file once
|
|
13864
|
+
(a side-effect import is enough) and the SDK becomes typed.
|
|
13865
|
+
|
|
13866
|
+
This emits TypeScript only \u2014 there is no codegen for other languages yet.
|
|
13867
|
+
|
|
13868
|
+
Examples:
|
|
13869
|
+
$ arkveil generate typescript -o src/arkveil.generated.ts
|
|
13870
|
+
$ arkveil gen ts --include user,context > src/arkveil.generated.ts
|
|
13871
|
+
$ arkveil generate typescript --json # { language, include, code, \u2026 }
|
|
13872
|
+
`
|
|
13873
|
+
).action(async (options, command) => {
|
|
13874
|
+
await run(command, (ctx) => generateTypeScript(ctx, options));
|
|
13875
|
+
});
|
|
13876
|
+
}
|
|
13877
|
+
|
|
13267
13878
|
// src/commands/folders/create.ts
|
|
13268
13879
|
async function createFolder(ctx, options) {
|
|
13269
13880
|
const body = {
|
|
@@ -13669,12 +14280,13 @@ async function deletePolicy(ctx, targetNodeId, policyId, options) {
|
|
|
13669
14280
|
// src/commands/policies/index.ts
|
|
13670
14281
|
var POLICY_TYPES = ["PERMISSION", "READ", "WRITE", "INVARIANT", "PROJECTION"];
|
|
13671
14282
|
var POLICY_STATUSES = ["ENABLED", "DISABLED", "DRAFT", "DELETED"];
|
|
14283
|
+
var DSL_HELP = "\n--condition and --filter use the Arkveil formula DSL.\nRun `arkveil formula syntax` for the full reference, or `arkveil formula parse` to validate a formula.\n";
|
|
13672
14284
|
function registerPolicies(program2) {
|
|
13673
14285
|
const policies = program2.command("policies").description("Manage policies attached to a target");
|
|
13674
|
-
policies.command("create <targetNodeId>").description("Create a policy under a target").addOption(new Option("--type <type>", "policy type").choices(POLICY_TYPES).makeOptionMandatory()).addOption(new Option("--status <status>", "policy status").choices(POLICY_STATUSES).makeOptionMandatory()).requiredOption("--title <title>", "policy title").option("--description <text>", "description").option("--condition <dsl>", "condition DSL").option("--filter <dsl>", "filter DSL (data policies)").option("--projection <json>", "projection: inline JSON, @file, or -").action(async (targetNodeId, options, command) => {
|
|
14286
|
+
policies.command("create <targetNodeId>").description("Create a policy under a target").addOption(new Option("--type <type>", "policy type").choices(POLICY_TYPES).makeOptionMandatory()).addOption(new Option("--status <status>", "policy status").choices(POLICY_STATUSES).makeOptionMandatory()).requiredOption("--title <title>", "policy title").option("--description <text>", "description").option("--condition <dsl>", "condition DSL").option("--filter <dsl>", "filter DSL (data policies)").option("--projection <json>", "projection: inline JSON, @file, or -").addHelpText("after", DSL_HELP).action(async (targetNodeId, options, command) => {
|
|
13675
14287
|
await run(command, (ctx) => createPolicy(ctx, targetNodeId, options));
|
|
13676
14288
|
});
|
|
13677
|
-
policies.command("update <targetNodeId> <policyId>").description("Update a policy").addOption(new Option("--status <status>", "policy status").choices(POLICY_STATUSES).makeOptionMandatory()).requiredOption("--title <title>", "policy title").option("--description <text>", "description").option("--condition <dsl>", "condition DSL").option("--filter <dsl>", "filter DSL (data policies)").option("--projection <json>", "projection: inline JSON, @file, or -").action(
|
|
14289
|
+
policies.command("update <targetNodeId> <policyId>").description("Update a policy").addOption(new Option("--status <status>", "policy status").choices(POLICY_STATUSES).makeOptionMandatory()).requiredOption("--title <title>", "policy title").option("--description <text>", "description").option("--condition <dsl>", "condition DSL").option("--filter <dsl>", "filter DSL (data policies)").option("--projection <json>", "projection: inline JSON, @file, or -").addHelpText("after", DSL_HELP).action(
|
|
13678
14290
|
async (targetNodeId, policyId, options, command) => {
|
|
13679
14291
|
await run(command, (ctx) => updatePolicy(ctx, targetNodeId, policyId, options));
|
|
13680
14292
|
}
|
|
@@ -14009,6 +14621,105 @@ async function parseFormula(ctx, options) {
|
|
|
14009
14621
|
ctx.out.data(ast, () => JSON.stringify(ast, null, 2));
|
|
14010
14622
|
}
|
|
14011
14623
|
|
|
14624
|
+
// src/commands/formula/syntax.ts
|
|
14625
|
+
var FORMULA_SYNTAX_REFERENCE = `Arkveil Formula DSL \u2014 syntax reference
|
|
14626
|
+
|
|
14627
|
+
A formula is a single boolean expression evaluated against the attributes of a
|
|
14628
|
+
request; it returns true or false. Formulas are used for policy conditions and
|
|
14629
|
+
filters, target conditions, and test selectors.
|
|
14630
|
+
|
|
14631
|
+
ATTRIBUTE REFERENCES
|
|
14632
|
+
Read request attributes through one of four roots. The dot is part of the
|
|
14633
|
+
keyword \u2014 write "user.role", not "user . role":
|
|
14634
|
+
|
|
14635
|
+
user.<path> e.g. user.role, user.profile.age
|
|
14636
|
+
context.<path> e.g. context.country, context.time.hour (note: "context.", not "ctx.")
|
|
14637
|
+
action.<path> e.g. action.name, action.tags
|
|
14638
|
+
request.<path> e.g. request.invoice.amount
|
|
14639
|
+
|
|
14640
|
+
Paths may be nested with dots: request.invoice.line.total
|
|
14641
|
+
|
|
14642
|
+
LITERALS
|
|
14643
|
+
String "double quoted" escape an inner quote with \\" e.g. "O\\"Brien"
|
|
14644
|
+
Integer 42 whole numbers only \u2014 no decimals, no negatives
|
|
14645
|
+
Boolean true false
|
|
14646
|
+
Array ["a","b"] [1,2,3] [true,false]
|
|
14647
|
+
\u2022 never empty
|
|
14648
|
+
\u2022 all elements must be the same type
|
|
14649
|
+
\u2022 elements are literals only (no attribute references inside an array)
|
|
14650
|
+
|
|
14651
|
+
COMPARISON
|
|
14652
|
+
= equal user.role = "admin"
|
|
14653
|
+
!= not equal user.status != "blocked"
|
|
14654
|
+
|
|
14655
|
+
Equality is a single "=" (NOT "=="). "==" is not valid.
|
|
14656
|
+
|
|
14657
|
+
STRING PREDICATES left <op> right
|
|
14658
|
+
contains request.path contains "/admin"
|
|
14659
|
+
containsIgnoreCase user.email containsIgnoreCase "@Arkveil.com"
|
|
14660
|
+
startsWith action.name startsWith "delete"
|
|
14661
|
+
startsWithIgnoreCase action.name startsWithIgnoreCase "Delete"
|
|
14662
|
+
matches user.name matches "^A.*" (regular expression)
|
|
14663
|
+
|
|
14664
|
+
PRESENCE / SET / COLLECTION CHECKS
|
|
14665
|
+
is null user.manager is null
|
|
14666
|
+
is not null user.manager is not null
|
|
14667
|
+
in <array> user.role in ["admin","editor"]
|
|
14668
|
+
not in <array> context.region not in ["EU","UK"]
|
|
14669
|
+
is empty action.tags is empty
|
|
14670
|
+
is not empty action.tags is not empty
|
|
14671
|
+
is uniform request.amounts is uniform (all elements are equal)
|
|
14672
|
+
is diverse request.amounts is diverse (elements are not all equal)
|
|
14673
|
+
|
|
14674
|
+
BOOLEAN LOGIC precedence, lowest to highest: or < and < not
|
|
14675
|
+
and user.active = true and context.country = "US"
|
|
14676
|
+
or user.isOwner = true or user.role = "admin"
|
|
14677
|
+
not not (user.suspended = true)
|
|
14678
|
+
|
|
14679
|
+
Keywords are lowercase. Use parentheses to group: (a or b) and c
|
|
14680
|
+
|
|
14681
|
+
ITERATIVE PREDICATES OVER COLLECTIONS
|
|
14682
|
+
Test the elements of an array. <condition> is a full boolean expression in
|
|
14683
|
+
which "it" refers to the current element:
|
|
14684
|
+
|
|
14685
|
+
any <collection> [as <alias>] where <condition> at least one element matches
|
|
14686
|
+
all <collection> [as <alias>] where <condition> every element matches
|
|
14687
|
+
every <collection> [as <alias>] where <condition> every element matches
|
|
14688
|
+
none <collection> [as <alias>] where <condition> no element matches
|
|
14689
|
+
exists <collection> [as <alias>] where <condition> at least one element matches
|
|
14690
|
+
|
|
14691
|
+
\u2022 <collection> must be an array attribute (user./context./action./request.)
|
|
14692
|
+
or an array literal. It cannot be "it", a scalar literal, or a parenthesized
|
|
14693
|
+
expression.
|
|
14694
|
+
\u2022 Because <condition> is a full expression, "and"/"or" bind INSIDE the where:
|
|
14695
|
+
any user.tags where it = "a" or it = "b"
|
|
14696
|
+
To combine a whole iterative predicate with an outer expression, wrap it in
|
|
14697
|
+
parentheses:
|
|
14698
|
+
(any user.tags where it = "a") or user.active = true
|
|
14699
|
+
\u2022 Each <collection> must be one of those roots or an array literal \u2014 you cannot
|
|
14700
|
+
iterate an alias element (e.g. "g.members" is not a valid collection).
|
|
14701
|
+
\u2022 For nested iteration, name the outer collection with "as" and reference its
|
|
14702
|
+
current element as <alias>.it ("it" alone is always the innermost element):
|
|
14703
|
+
any user.groups as g where any context.allowedGroups where it = g.it
|
|
14704
|
+
|
|
14705
|
+
EXAMPLES
|
|
14706
|
+
user.role = "admin"
|
|
14707
|
+
user.role = "admin" and context.country = "US"
|
|
14708
|
+
not (user.suspended = true) and user.role in ["admin","editor"]
|
|
14709
|
+
request.path startsWith "/admin/" and user.clearance != "none"
|
|
14710
|
+
action.tags is not empty and none action.tags where it = "blocked"
|
|
14711
|
+
any user.permissions where it startsWith "billing:"
|
|
14712
|
+
all request.items as line where line.it != ""
|
|
14713
|
+
(any user.tags where it = "vip") or user.isOwner = true
|
|
14714
|
+
`;
|
|
14715
|
+
function formulaSyntax(ctx) {
|
|
14716
|
+
ctx.out.data(
|
|
14717
|
+
{ dsl: "arkveil-formula", reference: FORMULA_SYNTAX_REFERENCE },
|
|
14718
|
+
() => FORMULA_SYNTAX_REFERENCE
|
|
14719
|
+
);
|
|
14720
|
+
return Promise.resolve();
|
|
14721
|
+
}
|
|
14722
|
+
|
|
14012
14723
|
// src/commands/formula/index.ts
|
|
14013
14724
|
var CONTEXTS = [
|
|
14014
14725
|
"ACTION_PERMISSION",
|
|
@@ -14020,12 +14731,15 @@ var CONTEXTS = [
|
|
|
14020
14731
|
];
|
|
14021
14732
|
function registerFormula(program2) {
|
|
14022
14733
|
const formula = program2.command("formula").description("Work with the formula DSL");
|
|
14734
|
+
formula.command("syntax").description("Print the formula DSL syntax reference").addHelpText(
|
|
14735
|
+
"after",
|
|
14736
|
+
"\nThe full DSL reference (operators, predicates, collections, examples) is\nprinted to stdout. Pipe it anywhere, e.g. `arkveil formula syntax | less`.\n"
|
|
14737
|
+
).action(async (_options, command) => {
|
|
14738
|
+
await run(command, formulaSyntax);
|
|
14739
|
+
});
|
|
14023
14740
|
formula.command("parse").description("Parse a formula DSL string into its AST").requiredOption("--dsl <dsl>", "the formula DSL to parse").addOption(new Option("--context <context>", "evaluation context").choices(CONTEXTS).makeOptionMandatory()).option("--request-schema <json>", "request schema: inline JSON, @file, or -").addHelpText(
|
|
14024
14741
|
"after",
|
|
14025
|
-
`
|
|
14026
|
-
Example:
|
|
14027
|
-
$ arkveil formula parse --context ACTION_PERMISSION --dsl 'user.role == "admin"'
|
|
14028
|
-
`
|
|
14742
|
+
"\nExample:\n $ arkveil formula parse --context ACTION_PERMISSION --dsl 'user.role = \"admin\"'\n\nSee `arkveil formula syntax` for the full DSL reference.\n"
|
|
14029
14743
|
).action(
|
|
14030
14744
|
async (options, command) => {
|
|
14031
14745
|
await run(command, (ctx) => parseFormula(ctx, options));
|
|
@@ -14273,6 +14987,8 @@ function buildProgram() {
|
|
|
14273
14987
|
registerTests(program2);
|
|
14274
14988
|
registerSettings(program2);
|
|
14275
14989
|
registerSchemas(program2);
|
|
14990
|
+
registerSdk(program2);
|
|
14991
|
+
registerGenerate(program2);
|
|
14276
14992
|
registerFormula(program2);
|
|
14277
14993
|
registerEval(program2);
|
|
14278
14994
|
registerAbac(program2);
|
|
@@ -14285,6 +15001,8 @@ Examples:
|
|
|
14285
15001
|
$ arkveil health Check API connectivity
|
|
14286
15002
|
$ arkveil tags list --json List tags as JSON
|
|
14287
15003
|
$ arkveil trees forest Show the full navigation forest
|
|
15004
|
+
$ arkveil sdk info How to install & use the SDK
|
|
15005
|
+
$ arkveil formula syntax Print the formula DSL reference
|
|
14288
15006
|
$ arkveil eval explain -a orders:read \\
|
|
14289
15007
|
--user '{"role":"admin"}' --context '{}' Explain an access decision
|
|
14290
15008
|
|