witnora 0.8.1 → 0.9.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.
Files changed (87) hide show
  1. package/README.md +275 -12
  2. package/dist/artifact-manifest.js +37 -0
  3. package/dist/artifact-validation.js +102 -0
  4. package/dist/badge.js +31 -0
  5. package/dist/browser-adapter.js +156 -0
  6. package/dist/bundle.js +100 -0
  7. package/dist/cli.js +971 -0
  8. package/dist/command-help.js +71 -0
  9. package/dist/companion-artifacts.js +170 -0
  10. package/dist/conformance.js +115 -0
  11. package/dist/control-plane.js +409 -0
  12. package/dist/corpus-governance.js +126 -0
  13. package/dist/corpus-store.js +239 -0
  14. package/dist/corpus.js +438 -0
  15. package/dist/credentials.js +127 -0
  16. package/dist/evidence-signing.js +85 -0
  17. package/dist/failure-review.js +188 -0
  18. package/dist/guided-setup.js +101 -0
  19. package/dist/index.js +16 -0
  20. package/dist/lab.js +323 -0
  21. package/dist/local-server.js +491 -0
  22. package/dist/monitor.js +100 -0
  23. package/dist/normalizers.js +265 -0
  24. package/dist/onboarding-templates.js +158 -0
  25. package/dist/release-gate.js +457 -0
  26. package/dist/report.js +165 -0
  27. package/dist/runner.js +349 -0
  28. package/dist/sandbox.js +424 -0
  29. package/dist/schema-validator.js +503 -0
  30. package/dist/try.js +135 -0
  31. package/dist/types.js +3 -0
  32. package/dist/vendor/onegent-runtime/browser-adapter-kit.d.ts +107 -0
  33. package/dist/vendor/onegent-runtime/browser-adapter-kit.d.ts.map +1 -0
  34. package/dist/vendor/onegent-runtime/browser-adapter-kit.js +187 -0
  35. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts +183 -0
  36. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts.map +1 -0
  37. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.js +344 -0
  38. package/dist/vendor/onegent-runtime/controlled-adapter.d.ts +6 -0
  39. package/dist/vendor/onegent-runtime/controlled-adapter.d.ts.map +1 -0
  40. package/dist/vendor/onegent-runtime/controlled-adapter.js +40 -0
  41. package/dist/vendor/onegent-runtime/mock-procurement.d.ts +6 -0
  42. package/dist/vendor/onegent-runtime/mock-procurement.d.ts.map +1 -0
  43. package/dist/vendor/onegent-runtime/mock-procurement.js +33 -0
  44. package/dist/vendor/onegent-runtime/policies.d.ts +6 -0
  45. package/dist/vendor/onegent-runtime/policies.d.ts.map +1 -0
  46. package/dist/vendor/onegent-runtime/policies.js +109 -0
  47. package/dist/vendor/onegent-runtime/risk.d.ts +3 -0
  48. package/dist/vendor/onegent-runtime/risk.d.ts.map +1 -0
  49. package/dist/vendor/onegent-runtime/risk.js +54 -0
  50. package/dist/vendor/onegent-runtime/sandbox-adapter-kit.d.ts +45 -0
  51. package/dist/vendor/onegent-runtime/sandbox-adapter-kit.d.ts.map +1 -0
  52. package/dist/vendor/onegent-runtime/sandbox-adapter-kit.js +148 -0
  53. package/dist/vendor/onegent-runtime/sandbox-harness.d.ts +167 -0
  54. package/dist/vendor/onegent-runtime/sandbox-harness.d.ts.map +1 -0
  55. package/dist/vendor/onegent-runtime/sandbox-harness.js +729 -0
  56. package/dist/vendor/onegent-runtime/sandbox-hosted.d.ts +77 -0
  57. package/dist/vendor/onegent-runtime/sandbox-hosted.d.ts.map +1 -0
  58. package/dist/vendor/onegent-runtime/sandbox-hosted.js +152 -0
  59. package/dist/vendor/onegent-runtime/sdk.d.ts +29 -0
  60. package/dist/vendor/onegent-runtime/sdk.d.ts.map +1 -0
  61. package/dist/vendor/onegent-runtime/sdk.js +140 -0
  62. package/dist/vendor/onegent-runtime/service.d.ts +27 -0
  63. package/dist/vendor/onegent-runtime/service.d.ts.map +1 -0
  64. package/dist/vendor/onegent-runtime/service.js +423 -0
  65. package/dist/vendor/onegent-runtime/store.d.ts +16 -0
  66. package/dist/vendor/onegent-runtime/store.d.ts.map +1 -0
  67. package/dist/vendor/onegent-runtime/store.js +29 -0
  68. package/dist/vendor/onegent-runtime/stripe-test-readonly.d.ts +79 -0
  69. package/dist/vendor/onegent-runtime/stripe-test-readonly.d.ts.map +1 -0
  70. package/dist/vendor/onegent-runtime/stripe-test-readonly.js +163 -0
  71. package/dist/vendor/onegent-runtime/trust-crypto.d.ts +11 -0
  72. package/dist/vendor/onegent-runtime/trust-crypto.d.ts.map +1 -0
  73. package/dist/vendor/onegent-runtime/trust-crypto.js +70 -0
  74. package/dist/vendor/onegent-runtime/trust-types.d.ts +163 -0
  75. package/dist/vendor/onegent-runtime/trust-types.d.ts.map +1 -0
  76. package/dist/vendor/onegent-runtime/trust-types.js +7 -0
  77. package/dist/vendor/onegent-runtime/trusted-recorder.d.ts +49 -0
  78. package/dist/vendor/onegent-runtime/trusted-recorder.d.ts.map +1 -0
  79. package/dist/vendor/onegent-runtime/trusted-recorder.js +347 -0
  80. package/dist/vendor/onegent-runtime/types.d.ts +287 -0
  81. package/dist/vendor/onegent-runtime/types.d.ts.map +1 -0
  82. package/dist/vendor/onegent-runtime/types.js +1 -0
  83. package/dist/vendor/onegent-runtime/vendor-sandbox-egress.d.ts +49 -0
  84. package/dist/vendor/onegent-runtime/vendor-sandbox-egress.d.ts.map +1 -0
  85. package/dist/vendor/onegent-runtime/vendor-sandbox-egress.js +152 -0
  86. package/package.json +33 -13
  87. package/bin/witnora.js +0 -3
@@ -0,0 +1,71 @@
1
+ export function renderCommandHelp(command) {
2
+ if (command === "sandbox" || command === "browser-adapter")
3
+ return undefined;
4
+ if (command === "setup")
5
+ return `Usage:
6
+ witnora setup apply --plan witnora-setup-plan.json --repo .
7
+ witnora setup apply --plan witnora-setup-plan.json --repo . --github-pr
8
+ witnora setup apply --plan witnora-setup-plan.json --repo . --github-pr --configure-ci
9
+
10
+ Applies only allowlisted Witnora templates from a typed setup plan. Commands embedded in the plan are never executed.
11
+ --github-pr requires authenticated git and GitHub CLI, creates a reviewable branch, and never writes API keys.
12
+ --configure-ci additionally reads WITNORA_PROJECT_ID and WITNORA_API_KEY from the environment, then stores them through GitHub Variables and Secrets. Legacy AGENTCERT_* names remain supported. The API key is passed to gh on standard input.
13
+ `;
14
+ if (command === "try")
15
+ return `Usage:
16
+ witnora try [--template <browser|coding|mcp|workflow|data>] [--subject <name>]
17
+ witnora try --template workflow --push
18
+
19
+ Runs a deterministic local sample without an agent, model key, or external system.
20
+ The output demonstrates the Witnora evidence path only; it is not a customer-agent evaluation.
21
+
22
+ Options:
23
+ --template <type> Sample boundary (default: workflow)
24
+ --subject <name> Sample identity (default: witnora-sample-<template>)
25
+ --out <directory> Output directory (default: .witnora/try)
26
+ --push Upload after a hosted project has been connected
27
+ --help, -h Show this help
28
+ `;
29
+ if (command === "init")
30
+ return `Usage:
31
+ witnora init --template <browser|coding|mcp|workflow|data> [--subject <name>]
32
+ witnora init --template browser --github-action
33
+
34
+ Options:
35
+ --template <type> External agent boundary (default: browser)
36
+ --subject <name> Agent identity (default: my-<template>-agent)
37
+ --out <path> Profile output (default: witnora.config.json)
38
+ --adapter-out <path> Envelope adapter for coding/workflow/data
39
+ --github-action Write the browser Tripwire workflow
40
+ --force Replace existing starter files
41
+ --help, -h Show this help without writing files
42
+ `;
43
+ if (command === "conformance")
44
+ return `Usage:
45
+ witnora conformance <evidence.json> --artifact-root <directory> [--implementation <name>] [--out <report.json>]
46
+
47
+ Checks schema identity, v0.1 compatibility, artifact manifest structure, and exact artifact hashes and sizes.
48
+ `;
49
+ if (command !== "push")
50
+ return undefined;
51
+ return `Usage:
52
+ witnora push --evidence .witnora/latest/agentcert-evidence.json
53
+ witnora push --evidence <path> --server <url> --project <project-id> [--api-key <key>]
54
+
55
+ Options:
56
+ --evidence <path> Evidence bundle (default: .witnora/latest/agentcert-evidence.json)
57
+ --connection <name> Saved hosted connection
58
+ --server <url> Hosted Witnora base URL
59
+ --project <id> Hosted project ID
60
+ --api-key <key> Project API key (prefer WITNORA_API_KEY in CI)
61
+ --external-id <id> Idempotent hosted run ID
62
+ --assurance-case <id> Issued assurance case to reconcile
63
+ --assurance-scope <p> Declared agent/model/prompt/tools/policy/suite scope JSON
64
+ --assurance-trigger <t> auto, pull_request, release, or nightly (default: auto)
65
+ --require-current Fail unless Hosted confirms authoritative CURRENT and complete evidence
66
+ --continuous-health-out <path> Write the redacted Hosted health contract
67
+ --artifact-root <dir> Allowed root for companion artifacts (default: current directory)
68
+ --no-artifacts Upload only the evidence bundle
69
+ --help, -h Show this help
70
+ `;
71
+ }
@@ -0,0 +1,170 @@
1
+ import { lstat, open, realpath, stat } from "node:fs/promises";
2
+ import { basename, extname, isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { collectEvidenceArtifactPaths, isRemoteArtifactPath, } from "./artifact-validation.js";
4
+ export const DEFAULT_COMPANION_ARTIFACT_LIMITS = {
5
+ maxFiles: 25,
6
+ maxFileBytes: 10 * 1024 * 1024,
7
+ maxTotalBytes: 50 * 1024 * 1024,
8
+ };
9
+ export const MAX_REPORTED_COMPANION_ARTIFACT_SKIPS = 50;
10
+ export async function collectCompanionArtifacts(bundle, artifactRoot, limits = DEFAULT_COMPANION_ARTIFACT_LIMITS) {
11
+ validateLimits(limits);
12
+ const root = await realpath(resolve(artifactRoot));
13
+ const artifacts = [];
14
+ const skipped = [];
15
+ const uploadedRealPaths = new Set();
16
+ let totalBytes = 0;
17
+ for (const entry of collectEvidenceArtifactPaths(bundle, root)) {
18
+ if (isRemoteArtifactPath(entry.sourcePath)) {
19
+ skipped.push(skip(entry, "remote_url"));
20
+ continue;
21
+ }
22
+ const descriptor = describeArtifact(entry.sourcePath);
23
+ if (!descriptor) {
24
+ skipped.push(skip(entry, "unsupported_type", "accepted formats: PNG, JPEG, WebP, JSON, JSONL, HTML, PDF, ZIP"));
25
+ continue;
26
+ }
27
+ if (artifacts.length >= limits.maxFiles) {
28
+ skipped.push(skip(entry, "file_limit", `maximum ${limits.maxFiles} files`));
29
+ continue;
30
+ }
31
+ const candidate = isAbsolute(entry.sourcePath)
32
+ ? resolve(entry.sourcePath)
33
+ : resolve(entry.root, entry.sourcePath);
34
+ if (!isWithinRoot(root, candidate)) {
35
+ skipped.push(skip(entry, "outside_artifact_root"));
36
+ continue;
37
+ }
38
+ let candidateInfo;
39
+ try {
40
+ candidateInfo = await lstat(candidate);
41
+ }
42
+ catch (error) {
43
+ skipped.push(skip(entry, errorCode(error) === "ENOENT" ? "missing" : "unreadable", errorMessage(error)));
44
+ continue;
45
+ }
46
+ if (candidateInfo.isSymbolicLink()) {
47
+ skipped.push(skip(entry, "symlink"));
48
+ continue;
49
+ }
50
+ let resolvedPath;
51
+ try {
52
+ resolvedPath = await realpath(candidate);
53
+ }
54
+ catch (error) {
55
+ skipped.push(skip(entry, errorCode(error) === "ENOENT" ? "missing" : "unreadable", errorMessage(error)));
56
+ continue;
57
+ }
58
+ if (!isWithinRoot(root, resolvedPath)) {
59
+ skipped.push(skip(entry, "outside_artifact_root"));
60
+ continue;
61
+ }
62
+ if (uploadedRealPaths.has(resolvedPath))
63
+ continue;
64
+ let fileInfo;
65
+ try {
66
+ fileInfo = await stat(resolvedPath);
67
+ }
68
+ catch (error) {
69
+ skipped.push(skip(entry, errorCode(error) === "ENOENT" ? "missing" : "unreadable", errorMessage(error)));
70
+ continue;
71
+ }
72
+ if (!fileInfo.isFile()) {
73
+ skipped.push(skip(entry, "not_file"));
74
+ continue;
75
+ }
76
+ if (fileInfo.size > limits.maxFileBytes) {
77
+ skipped.push(skip(entry, "file_too_large", `${fileInfo.size} bytes exceeds ${limits.maxFileBytes}`));
78
+ continue;
79
+ }
80
+ if (totalBytes + fileInfo.size > limits.maxTotalBytes) {
81
+ skipped.push(skip(entry, "total_size_limit", `maximum ${limits.maxTotalBytes} bytes`));
82
+ continue;
83
+ }
84
+ const remainingBytes = limits.maxTotalBytes - totalBytes;
85
+ let readResult;
86
+ try {
87
+ readResult = await readBounded(resolvedPath, Math.min(limits.maxFileBytes, remainingBytes));
88
+ }
89
+ catch (error) {
90
+ skipped.push(skip(entry, "unreadable", errorMessage(error)));
91
+ continue;
92
+ }
93
+ if (readResult.exceeded) {
94
+ const reason = remainingBytes < limits.maxFileBytes ? "total_size_limit" : "file_too_large";
95
+ skipped.push(skip(entry, reason));
96
+ continue;
97
+ }
98
+ artifacts.push({
99
+ sourcePath: normalizeSourcePath(entry.sourcePath),
100
+ fileName: basename(resolvedPath),
101
+ kind: descriptor.kind,
102
+ contentType: descriptor.contentType,
103
+ bytes: readResult.bytes,
104
+ });
105
+ uploadedRealPaths.add(resolvedPath);
106
+ totalBytes += readResult.bytes.byteLength;
107
+ }
108
+ return { artifacts, skipped, totalBytes, limits: { ...limits } };
109
+ }
110
+ function validateLimits(limits) {
111
+ for (const [name, value] of Object.entries(limits)) {
112
+ if (!Number.isSafeInteger(value) || value <= 0)
113
+ throw new Error(`${name} must be a positive integer.`);
114
+ }
115
+ }
116
+ function isWithinRoot(root, candidate) {
117
+ const pathFromRoot = relative(root, candidate);
118
+ return pathFromRoot === "" || (!pathFromRoot.startsWith(`..${sep}`) && pathFromRoot !== ".." && !isAbsolute(pathFromRoot));
119
+ }
120
+ async function readBounded(path, maxBytes) {
121
+ const handle = await open(path, "r");
122
+ const chunks = [];
123
+ let total = 0;
124
+ try {
125
+ while (total <= maxBytes) {
126
+ const capacity = Math.min(64 * 1024, maxBytes + 1 - total);
127
+ const chunk = Buffer.allocUnsafe(capacity);
128
+ const { bytesRead } = await handle.read(chunk, 0, capacity, null);
129
+ if (bytesRead === 0)
130
+ return { bytes: Buffer.concat(chunks, total), exceeded: false };
131
+ chunks.push(chunk.subarray(0, bytesRead));
132
+ total += bytesRead;
133
+ }
134
+ return { bytes: new Uint8Array(), exceeded: true };
135
+ }
136
+ finally {
137
+ await handle.close();
138
+ }
139
+ }
140
+ function describeArtifact(path) {
141
+ const lower = path.toLowerCase();
142
+ const extension = extname(lower);
143
+ if ([".png", ".jpg", ".jpeg", ".webp"].includes(extension)) {
144
+ const imageType = extension === ".jpg" ? "jpeg" : extension.slice(1);
145
+ return { kind: "screenshot", contentType: `image/${imageType}` };
146
+ }
147
+ if (extension === ".zip")
148
+ return { kind: "trace", contentType: "application/zip" };
149
+ if ([".html", ".htm"].includes(extension))
150
+ return { kind: "dom", contentType: "text/html; charset=utf-8" };
151
+ if (extension === ".json")
152
+ return { kind: lower.includes("trace") ? "trace" : lower.includes("dom") ? "dom" : "json", contentType: "application/json" };
153
+ if (extension === ".jsonl")
154
+ return { kind: lower.includes("trace") ? "trace" : "json", contentType: "application/x-ndjson" };
155
+ if (extension === ".pdf")
156
+ return { kind: "report", contentType: "application/pdf" };
157
+ return undefined;
158
+ }
159
+ function normalizeSourcePath(path) {
160
+ return path.replace(/\\/g, "/").slice(0, 1024);
161
+ }
162
+ function skip(entry, reason, detail) {
163
+ return { sourcePath: normalizeSourcePath(entry.sourcePath), reason, ...(detail ? { detail } : {}) };
164
+ }
165
+ function errorCode(error) {
166
+ return error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
167
+ }
168
+ function errorMessage(error) {
169
+ return error instanceof Error ? error.message : String(error);
170
+ }
@@ -0,0 +1,115 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { normalizeManifestPath } from "./artifact-manifest.js";
5
+ import { validateAgentCertSchema } from "./schema-validator.js";
6
+ const SUPPORTED_TOP_LEVEL_FIELDS = new Set([
7
+ "schemaName", "schemaVersion", "schemaSemver", "kind", "runId", "generatedAt", "subject", "verdict", "summary",
8
+ "results", "evidence", "artifacts", "artifactManifest", "standards",
9
+ ]);
10
+ export async function runEvidenceConformance(input, options) {
11
+ const checks = [];
12
+ const schema = validateAgentCertSchema("evidence-bundle", input);
13
+ checks.push(check("schema", "Evidence bundle satisfies the AgentCert v0.1 semantic contract.", schema.errors));
14
+ const bundle = object(input);
15
+ const compatibilityErrors = bundle
16
+ ? Object.keys(bundle).filter((key) => !SUPPORTED_TOP_LEVEL_FIELDS.has(key)).map((key) => `Unsupported top-level field: ${key}.`)
17
+ : ["Evidence bundle must be an object."];
18
+ checks.push(check("compatibility", "Evidence bundle uses only v0.1-compatible top-level fields.", compatibilityErrors));
19
+ const manifestErrors = [];
20
+ const entries = parseManifest(bundle?.artifactManifest, manifestErrors);
21
+ checks.push(check("manifest", "Artifact manifest declares unique normalized paths, SHA-256, byte size, and kind.", manifestErrors));
22
+ const byteErrors = [];
23
+ if (manifestErrors.length === 0) {
24
+ const root = resolve(options.artifactRoot);
25
+ for (const entry of entries) {
26
+ const path = resolve(root, entry.path);
27
+ if (path !== root && !path.startsWith(`${root}\\`) && !path.startsWith(`${root}/`)) {
28
+ byteErrors.push(`${entry.path}: resolved path escapes artifact root.`);
29
+ continue;
30
+ }
31
+ try {
32
+ const bytes = await readFile(path);
33
+ const digest = createHash("sha256").update(bytes).digest("hex");
34
+ if (bytes.byteLength !== entry.sizeBytes)
35
+ byteErrors.push(`${entry.path}: expected ${entry.sizeBytes} bytes, observed ${bytes.byteLength}.`);
36
+ if (digest !== entry.sha256)
37
+ byteErrors.push(`${entry.path}: SHA-256 does not match the declared digest.`);
38
+ }
39
+ catch (error) {
40
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : "read_failed";
41
+ byteErrors.push(`${entry.path}: artifact could not be read (${code}).`);
42
+ }
43
+ }
44
+ }
45
+ else {
46
+ byteErrors.push("Artifact bytes were not checked because the manifest is invalid.");
47
+ }
48
+ checks.push(check("artifact-bytes", "Every declared artifact matches its exact runtime bytes.", byteErrors));
49
+ const passed = checks.filter((item) => item.status === "passed").length;
50
+ const failed = checks.length - passed;
51
+ return {
52
+ schemaVersion: "agentcert.conformance.v0.1",
53
+ kind: "agentcert.evidence_conformance_report",
54
+ implementation: options.implementation?.trim() || "third-party",
55
+ evidenceFile: resolve(options.evidenceFile),
56
+ artifactRoot: resolve(options.artifactRoot),
57
+ generatedAt: (options.now ?? new Date()).toISOString(),
58
+ valid: failed === 0,
59
+ summary: { passed, failed },
60
+ checks,
61
+ };
62
+ }
63
+ function parseManifest(input, errors) {
64
+ const manifest = object(input);
65
+ if (!manifest) {
66
+ errors.push("artifactManifest is required for conformance.");
67
+ return [];
68
+ }
69
+ if (manifest.schemaVersion !== "agentcert.artifact_manifest.v0.1")
70
+ errors.push("artifactManifest.schemaVersion must be agentcert.artifact_manifest.v0.1.");
71
+ if (!Array.isArray(manifest.entries)) {
72
+ errors.push("artifactManifest.entries must be an array.");
73
+ return [];
74
+ }
75
+ if (manifest.entries.length > 500)
76
+ errors.push("artifactManifest.entries cannot exceed 500 entries.");
77
+ const paths = new Set();
78
+ const entries = [];
79
+ manifest.entries.forEach((inputEntry, index) => {
80
+ const entry = object(inputEntry);
81
+ if (!entry) {
82
+ errors.push(`artifactManifest.entries[${index}] must be an object.`);
83
+ return;
84
+ }
85
+ let path;
86
+ try {
87
+ path = typeof entry.path === "string" ? normalizeManifestPath(entry.path) : undefined;
88
+ }
89
+ catch (error) {
90
+ errors.push(`artifactManifest.entries[${index}].path: ${error.message}`);
91
+ }
92
+ if (!path)
93
+ errors.push(`artifactManifest.entries[${index}].path must be a normalized relative path.`);
94
+ else if (paths.has(path))
95
+ errors.push(`artifactManifest contains duplicate path ${path}.`);
96
+ else
97
+ paths.add(path);
98
+ if (typeof entry.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(entry.sha256))
99
+ errors.push(`artifactManifest.entries[${index}].sha256 must be 64 lowercase hex characters.`);
100
+ if (!Number.isInteger(entry.sizeBytes) || entry.sizeBytes < 0)
101
+ errors.push(`artifactManifest.entries[${index}].sizeBytes must be a non-negative integer.`);
102
+ if (typeof entry.kind !== "string" || !/^[a-z][a-z0-9_-]{0,63}$/.test(entry.kind))
103
+ errors.push(`artifactManifest.entries[${index}].kind must be a stable lowercase identifier.`);
104
+ if (path && typeof entry.sha256 === "string" && Number.isInteger(entry.sizeBytes) && typeof entry.kind === "string") {
105
+ entries.push({ path, sha256: entry.sha256, sizeBytes: entry.sizeBytes, kind: entry.kind });
106
+ }
107
+ });
108
+ return entries;
109
+ }
110
+ function check(id, message, errors) {
111
+ return { id, status: errors.length === 0 ? "passed" : "failed", message, errors };
112
+ }
113
+ function object(input) {
114
+ return input && typeof input === "object" && !Array.isArray(input) ? input : undefined;
115
+ }