@arkveil/cli 1.0.0 → 1.2.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 +987 -24
- 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
|
}
|
|
@@ -5413,9 +5413,9 @@ var require_src = __commonJS({
|
|
|
5413
5413
|
});
|
|
5414
5414
|
|
|
5415
5415
|
// src/index.ts
|
|
5416
|
-
import { readFileSync as
|
|
5417
|
-
import { fileURLToPath as
|
|
5418
|
-
import { dirname, join as
|
|
5416
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
5417
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
5418
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
5419
5419
|
|
|
5420
5420
|
// node_modules/.pnpm/commander@12.1.0/node_modules/commander/esm.mjs
|
|
5421
5421
|
var import_index = __toESM(require_commander(), 1);
|
|
@@ -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));
|
|
@@ -14240,12 +14954,255 @@ function registerAdmin(program2) {
|
|
|
14240
14954
|
});
|
|
14241
14955
|
}
|
|
14242
14956
|
|
|
14957
|
+
// src/commands/update/update.ts
|
|
14958
|
+
import { execFile as execFile6 } from "child_process";
|
|
14959
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
14960
|
+
|
|
14961
|
+
// src/commands/update/npm.ts
|
|
14962
|
+
import { existsSync, readFileSync as readFileSync4 } from "fs";
|
|
14963
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
14964
|
+
import { dirname, join as join2, parse } from "path";
|
|
14965
|
+
function readPackageMeta() {
|
|
14966
|
+
let dir = dirname(fileURLToPath2(import.meta.url));
|
|
14967
|
+
const { root } = parse(dir);
|
|
14968
|
+
while (true) {
|
|
14969
|
+
const candidate = join2(dir, "package.json");
|
|
14970
|
+
if (existsSync(candidate)) {
|
|
14971
|
+
const pkg = JSON.parse(readFileSync4(candidate, "utf8"));
|
|
14972
|
+
return { name: pkg.name ?? "@arkveil/cli", version: pkg.version ?? "0.0.0" };
|
|
14973
|
+
}
|
|
14974
|
+
if (dir === root) break;
|
|
14975
|
+
dir = dirname(dir);
|
|
14976
|
+
}
|
|
14977
|
+
return { name: "@arkveil/cli", version: "0.0.0" };
|
|
14978
|
+
}
|
|
14979
|
+
function parseSemver(version) {
|
|
14980
|
+
const cleaned = version.trim().replace(/^v/, "");
|
|
14981
|
+
const [main3 = "", pre = null] = cleaned.split("-", 2);
|
|
14982
|
+
const core = main3.split(".").map((p) => Number.parseInt(p, 10) || 0);
|
|
14983
|
+
while (core.length < 3) core.push(0);
|
|
14984
|
+
return { core: core.slice(0, 3), pre: pre ?? null };
|
|
14985
|
+
}
|
|
14986
|
+
function compareSemver(a3, b3) {
|
|
14987
|
+
const pa = parseSemver(a3);
|
|
14988
|
+
const pb = parseSemver(b3);
|
|
14989
|
+
for (let i = 0; i < 3; i++) {
|
|
14990
|
+
const diff = (pa.core[i] ?? 0) - (pb.core[i] ?? 0);
|
|
14991
|
+
if (diff !== 0) return diff < 0 ? -1 : 1;
|
|
14992
|
+
}
|
|
14993
|
+
if (pa.pre === pb.pre) return 0;
|
|
14994
|
+
if (pa.pre === null) return 1;
|
|
14995
|
+
if (pb.pre === null) return -1;
|
|
14996
|
+
return pa.pre < pb.pre ? -1 : 1;
|
|
14997
|
+
}
|
|
14998
|
+
var PACKAGE_MANAGERS = ["pnpm", "yarn", "bun", "npm"];
|
|
14999
|
+
function detectPackageManager(modulePath, env2 = process.env) {
|
|
15000
|
+
const path2 = modulePath.toLowerCase();
|
|
15001
|
+
if (path2.includes("pnpm")) return "pnpm";
|
|
15002
|
+
if (path2.includes(`${"/"}.bun${"/"}`) || path2.includes("/bun/")) return "bun";
|
|
15003
|
+
if (path2.includes(".yarn") || path2.includes("/yarn/")) return "yarn";
|
|
15004
|
+
const agent = (env2.npm_config_user_agent ?? "").toLowerCase();
|
|
15005
|
+
const fromAgent = PACKAGE_MANAGERS.find((pm) => agent.startsWith(pm));
|
|
15006
|
+
if (fromAgent) return fromAgent;
|
|
15007
|
+
return "npm";
|
|
15008
|
+
}
|
|
15009
|
+
function installCommand(pm, spec) {
|
|
15010
|
+
switch (pm) {
|
|
15011
|
+
case "pnpm":
|
|
15012
|
+
return { cmd: "pnpm", args: ["add", "-g", spec] };
|
|
15013
|
+
case "yarn":
|
|
15014
|
+
return { cmd: "yarn", args: ["global", "add", spec] };
|
|
15015
|
+
case "bun":
|
|
15016
|
+
return { cmd: "bun", args: ["add", "-g", spec] };
|
|
15017
|
+
case "npm":
|
|
15018
|
+
default:
|
|
15019
|
+
return { cmd: "npm", args: ["install", "-g", spec] };
|
|
15020
|
+
}
|
|
15021
|
+
}
|
|
15022
|
+
async function fetchLatestVersion(opts) {
|
|
15023
|
+
const tag = opts.tag ?? "latest";
|
|
15024
|
+
const registry = (opts.registry ?? "https://registry.npmjs.org").replace(/\/+$/, "");
|
|
15025
|
+
const encoded = opts.name.replace("/", "%2f");
|
|
15026
|
+
const url = `${registry}/${encoded}/${tag}`;
|
|
15027
|
+
const controller = new AbortController();
|
|
15028
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 1e4);
|
|
15029
|
+
try {
|
|
15030
|
+
const res = await fetch(url, {
|
|
15031
|
+
headers: { accept: "application/json" },
|
|
15032
|
+
signal: controller.signal
|
|
15033
|
+
});
|
|
15034
|
+
if (!res.ok) {
|
|
15035
|
+
throw new NetworkError(
|
|
15036
|
+
`npm registry returned ${res.status} for ${opts.name}@${tag}.`,
|
|
15037
|
+
"Check your network connection and that the package/tag exists."
|
|
15038
|
+
);
|
|
15039
|
+
}
|
|
15040
|
+
const body = await res.json();
|
|
15041
|
+
if (!body.version) {
|
|
15042
|
+
throw new NetworkError(
|
|
15043
|
+
`npm registry response for ${opts.name}@${tag} had no version.`
|
|
15044
|
+
);
|
|
15045
|
+
}
|
|
15046
|
+
return body.version;
|
|
15047
|
+
} catch (err) {
|
|
15048
|
+
if (err instanceof NetworkError) throw err;
|
|
15049
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
15050
|
+
throw new NetworkError(
|
|
15051
|
+
`Timed out contacting the npm registry for ${opts.name}@${tag}.`,
|
|
15052
|
+
"Retry, or increase the timeout with --timeout <ms>.",
|
|
15053
|
+
err
|
|
15054
|
+
);
|
|
15055
|
+
}
|
|
15056
|
+
throw new NetworkError(
|
|
15057
|
+
`Could not reach the npm registry: ${err instanceof Error ? err.message : String(err)}`,
|
|
15058
|
+
"Check your network connection.",
|
|
15059
|
+
err
|
|
15060
|
+
);
|
|
15061
|
+
} finally {
|
|
15062
|
+
clearTimeout(timer);
|
|
15063
|
+
}
|
|
15064
|
+
}
|
|
15065
|
+
|
|
15066
|
+
// src/commands/update/update.ts
|
|
15067
|
+
var VALID_PMS = ["npm", "pnpm", "yarn", "bun"];
|
|
15068
|
+
async function selfUpdate(ctx, options) {
|
|
15069
|
+
const { out, config } = ctx;
|
|
15070
|
+
const meta = readPackageMeta();
|
|
15071
|
+
const tag = options.tag ?? "latest";
|
|
15072
|
+
if (options.use && !VALID_PMS.includes(options.use)) {
|
|
15073
|
+
throw new CliError(`Unknown package manager "${options.use}".`, {
|
|
15074
|
+
exitCode: ExitCode.Usage,
|
|
15075
|
+
hint: `Choose one of: ${VALID_PMS.join(", ")}.`
|
|
15076
|
+
});
|
|
15077
|
+
}
|
|
15078
|
+
const spinner = out.spinner(`Checking npm for ${meta.name}@${tag}\u2026`);
|
|
15079
|
+
let latest;
|
|
15080
|
+
try {
|
|
15081
|
+
latest = await fetchLatestVersion({
|
|
15082
|
+
name: meta.name,
|
|
15083
|
+
tag,
|
|
15084
|
+
timeoutMs: config.timeoutMs
|
|
15085
|
+
});
|
|
15086
|
+
spinner.stop();
|
|
15087
|
+
} catch (err) {
|
|
15088
|
+
spinner.fail("Could not check for updates.");
|
|
15089
|
+
throw err;
|
|
15090
|
+
}
|
|
15091
|
+
const cmp = compareSemver(meta.version, latest);
|
|
15092
|
+
const upToDate = cmp >= 0;
|
|
15093
|
+
const willUpdate = !upToDate || Boolean(options.force);
|
|
15094
|
+
const pm = options.use ?? detectPackageManager(fileURLToPath3(import.meta.url));
|
|
15095
|
+
const spec = `${meta.name}@${tag}`;
|
|
15096
|
+
const { cmd, args } = installCommand(pm, spec);
|
|
15097
|
+
const commandLine = `${cmd} ${args.join(" ")}`;
|
|
15098
|
+
if (options.check || options.dryRun || !willUpdate) {
|
|
15099
|
+
out.data(
|
|
15100
|
+
{
|
|
15101
|
+
name: meta.name,
|
|
15102
|
+
current: meta.version,
|
|
15103
|
+
latest,
|
|
15104
|
+
tag,
|
|
15105
|
+
updateAvailable: !upToDate,
|
|
15106
|
+
upToDate,
|
|
15107
|
+
packageManager: pm,
|
|
15108
|
+
command: commandLine,
|
|
15109
|
+
action: options.dryRun ? "dry-run" : options.check ? "check" : "none"
|
|
15110
|
+
},
|
|
15111
|
+
(o2) => {
|
|
15112
|
+
if (upToDate) {
|
|
15113
|
+
return `${o2.c.green("\u2714")} ${meta.name} is up to date (${meta.version}).`;
|
|
15114
|
+
}
|
|
15115
|
+
const lines = [
|
|
15116
|
+
`${o2.c.yellow("!")} Update available: ${o2.c.dim(meta.version)} \u2192 ${o2.c.green(latest)}`
|
|
15117
|
+
];
|
|
15118
|
+
if (options.dryRun) {
|
|
15119
|
+
lines.push(` Would run: ${o2.c.cyan(commandLine)}`);
|
|
15120
|
+
} else {
|
|
15121
|
+
lines.push(` Run ${o2.c.cyan("arkveil update")} to upgrade (uses: ${o2.c.cyan(commandLine)}).`);
|
|
15122
|
+
}
|
|
15123
|
+
return lines.join("\n");
|
|
15124
|
+
}
|
|
15125
|
+
);
|
|
15126
|
+
return;
|
|
15127
|
+
}
|
|
15128
|
+
const label = upToDate ? `Reinstalling ${spec}\u2026` : `Updating ${meta.name} ${meta.version} \u2192 ${latest}\u2026`;
|
|
15129
|
+
out.verbose(`Running: ${commandLine}`);
|
|
15130
|
+
const install = out.spinner(label);
|
|
15131
|
+
try {
|
|
15132
|
+
await runInstall(cmd, args);
|
|
15133
|
+
install.succeed(`Updated to ${latest}. Run \`arkveil --version\` to confirm.`);
|
|
15134
|
+
} catch (err) {
|
|
15135
|
+
install.fail("Update failed.");
|
|
15136
|
+
throw toInstallError(err, pm, commandLine);
|
|
15137
|
+
}
|
|
15138
|
+
out.data(
|
|
15139
|
+
{
|
|
15140
|
+
name: meta.name,
|
|
15141
|
+
previous: meta.version,
|
|
15142
|
+
latest,
|
|
15143
|
+
tag,
|
|
15144
|
+
packageManager: pm,
|
|
15145
|
+
command: commandLine,
|
|
15146
|
+
action: "updated"
|
|
15147
|
+
},
|
|
15148
|
+
() => {
|
|
15149
|
+
}
|
|
15150
|
+
);
|
|
15151
|
+
}
|
|
15152
|
+
function runInstall(cmd, args) {
|
|
15153
|
+
return new Promise((resolve2, reject) => {
|
|
15154
|
+
execFile6(cmd, args, { timeout: 12e4 }, (err) => {
|
|
15155
|
+
if (err) reject(err);
|
|
15156
|
+
else resolve2();
|
|
15157
|
+
});
|
|
15158
|
+
});
|
|
15159
|
+
}
|
|
15160
|
+
function toInstallError(err, pm, commandLine) {
|
|
15161
|
+
const e2 = err;
|
|
15162
|
+
if (e2?.code === "ENOENT") {
|
|
15163
|
+
return new CliError(`${pm} was not found on your PATH.`, {
|
|
15164
|
+
exitCode: ExitCode.Generic,
|
|
15165
|
+
hint: `Install ${pm}, or pass --use <npm|pnpm|yarn|bun> to pick another package manager.`,
|
|
15166
|
+
cause: err
|
|
15167
|
+
});
|
|
15168
|
+
}
|
|
15169
|
+
const detail = (e2?.stderr ?? e2?.message ?? String(err)).trim().split("\n").slice(-3).join(" ");
|
|
15170
|
+
return new CliError(`Upgrade command failed: ${detail || commandLine}`, {
|
|
15171
|
+
exitCode: ExitCode.Generic,
|
|
15172
|
+
hint: `Try running it yourself: ${commandLine}. A global install may require elevated permissions (sudo).`,
|
|
15173
|
+
cause: err
|
|
15174
|
+
});
|
|
15175
|
+
}
|
|
15176
|
+
|
|
15177
|
+
// src/commands/update/index.ts
|
|
15178
|
+
function registerUpdate(program2) {
|
|
15179
|
+
program2.command("update").aliases(["upgrade", "self-update"]).description("Update the Arkveil CLI to the latest published version").option("--check", "only report whether an update is available; don't install").option("--dry-run", "print the install command that would run, without executing it").option("--tag <tag>", "npm dist-tag to install (default latest)", "latest").option("--use <pm>", "package manager to use: npm, pnpm, yarn, or bun").option("--force", "reinstall even if already on the target version").addHelpText(
|
|
15180
|
+
"after",
|
|
15181
|
+
`
|
|
15182
|
+
Checks the npm registry for a newer release of the CLI and upgrades this install
|
|
15183
|
+
in place using the package manager that installed it (auto-detected; override
|
|
15184
|
+
with --use). Use --check in scripts/CI to detect a pending update via the exit
|
|
15185
|
+
output without changing anything.
|
|
15186
|
+
|
|
15187
|
+
Examples:
|
|
15188
|
+
$ arkveil update # upgrade to the latest release
|
|
15189
|
+
$ arkveil update --check # is a newer version available?
|
|
15190
|
+
$ arkveil update --dry-run # show the command without running it
|
|
15191
|
+
$ arkveil update --tag next # install the "next" dist-tag
|
|
15192
|
+
$ arkveil update --use pnpm # force pnpm for the global install
|
|
15193
|
+
$ arkveil update --json # machine-readable result
|
|
15194
|
+
`
|
|
15195
|
+
).action(async (options, command) => {
|
|
15196
|
+
await run(command, (ctx) => selfUpdate(ctx, options));
|
|
15197
|
+
});
|
|
15198
|
+
}
|
|
15199
|
+
|
|
14243
15200
|
// src/index.ts
|
|
14244
15201
|
function readVersion() {
|
|
14245
15202
|
try {
|
|
14246
|
-
const here =
|
|
15203
|
+
const here = dirname2(fileURLToPath4(import.meta.url));
|
|
14247
15204
|
const pkg = JSON.parse(
|
|
14248
|
-
|
|
15205
|
+
readFileSync5(join3(here, "..", "package.json"), "utf8")
|
|
14249
15206
|
);
|
|
14250
15207
|
return pkg.version ?? "0.0.0";
|
|
14251
15208
|
} catch {
|
|
@@ -14273,10 +15230,13 @@ function buildProgram() {
|
|
|
14273
15230
|
registerTests(program2);
|
|
14274
15231
|
registerSettings(program2);
|
|
14275
15232
|
registerSchemas(program2);
|
|
15233
|
+
registerSdk(program2);
|
|
15234
|
+
registerGenerate(program2);
|
|
14276
15235
|
registerFormula(program2);
|
|
14277
15236
|
registerEval(program2);
|
|
14278
15237
|
registerAbac(program2);
|
|
14279
15238
|
registerAdmin(program2);
|
|
15239
|
+
registerUpdate(program2);
|
|
14280
15240
|
program2.addHelpText(
|
|
14281
15241
|
"after",
|
|
14282
15242
|
`
|
|
@@ -14285,6 +15245,9 @@ Examples:
|
|
|
14285
15245
|
$ arkveil health Check API connectivity
|
|
14286
15246
|
$ arkveil tags list --json List tags as JSON
|
|
14287
15247
|
$ arkveil trees forest Show the full navigation forest
|
|
15248
|
+
$ arkveil sdk info How to install & use the SDK
|
|
15249
|
+
$ arkveil update Update the CLI to the latest release
|
|
15250
|
+
$ arkveil formula syntax Print the formula DSL reference
|
|
14288
15251
|
$ arkveil eval explain -a orders:read \\
|
|
14289
15252
|
--user '{"role":"admin"}' --context '{}' Explain an access decision
|
|
14290
15253
|
|