border-customs 0.5.1 → 0.7.1

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.
@@ -0,0 +1,146 @@
1
+ // provenance: .omo/plans/border-exfil-lens.md T1 — scan surfaces for the native
2
+ // exfil legs: scanTreeText (blob/tree facet) and scanMessageText (commit
3
+ // message facet). Findings carry facet+channel attribution resolved through
4
+ // severity.ts's matrix — the ONLY emitter of severities in this package; the
5
+ // predicates in rules.ts decide detection, the matrix decides what is emitted
6
+ // where and how loud (plan §"分面契约").
7
+ //
8
+ // Native emission per the matrix:
9
+ // * tree facet → MEDIUM family only (home-path, cred-location,
10
+ // host-profile). The HIGH family's tree face belongs to
11
+ // the twin engines (T2); the v2 native double-emission
12
+ // was revoked, and a native tree rfc1918/ssh-target
13
+ // finding is UNREPRESENTABLE here (emitRulesFor returns
14
+ // none for those cells).
15
+ // * message facet → all five rules under the `exfil-*:message` id family;
16
+ // attribution `source` is the commit sha, which the
17
+ // pipeline passes through toRepoRelative unchanged
18
+ // (exclusions.ts:20-25), enabling per-commit allow rows.
19
+ // * tagNote facet → never scanned by the core (engine tag-message-secret
20
+ // leg owns it, tagScan.ts:26,101); there is deliberately
21
+ // no scanTagNoteText export.
22
+ //
23
+ // G23 masking invariant: the matched value leaves this module only as
24
+ // {valueDigest, snippet}. maskValue() mirrors redact.ts exactly (code-point
25
+ // based, ≤12 fully masked, else first4…last4) but is re-implemented locally
26
+ // because the import-audit pins src/exfil/** to node builtins + local files;
27
+ // the equivalence is pinned by a guard test against the real redact().
28
+ import { createHash } from "node:crypto";
29
+
30
+ import {
31
+ emitRulesFor,
32
+ exfilFindingId,
33
+ EXFIL_SECRET_VALUE_RULES,
34
+ observedSeverity,
35
+ type ExfilChannel,
36
+ type ExfilFacet,
37
+ type ExfilRuleId,
38
+ type ExfilSeverity,
39
+ } from "./severity.ts";
40
+ import {
41
+ matchCredLocation,
42
+ matchHomePath,
43
+ matchHostProfile,
44
+ matchRfc1918,
45
+ matchSshTarget,
46
+ type RuleHit,
47
+ type RuleOptions,
48
+ } from "./rules.ts";
49
+
50
+ /** Engine label for native exfil findings (mirrors "ai-artifacts" naming style). */
51
+ export const EXFIL_ENGINE = "border-exfil";
52
+
53
+ const FULL_MASK = "\u25ae\u25ae\u25ae\u25ae";
54
+ const ELLIPSIS = "\u2026";
55
+
56
+ /** redact.ts-equivalent masking (G23): sha256 digest + code-point-safe snippet. */
57
+ export function maskValue(value: string): { valueDigest: string; snippet: string } {
58
+ const valueDigest = createHash("sha256").update(value, "utf8").digest("hex");
59
+ const points = [...value];
60
+ const snippet = points.length <= 12 ? FULL_MASK : `${points.slice(0, 4).join("")}${ELLIPSIS}${points.slice(-4).join("")}`;
61
+ return { valueDigest, snippet };
62
+ }
63
+
64
+ export type ExfilScanInput = {
65
+ /** the text being audited (blob content or one commit message). */
66
+ readonly text: string;
67
+ /** attribution: file path (tree) or commit sha (message). */
68
+ readonly source: string;
69
+ /** operator's rules.hosts (config.ts:102) for the ssh-target predicate. */
70
+ readonly hosts?: readonly string[];
71
+ /**
72
+ * Ingest hook mirroring the redact.ts adapter contract ("every adapter
73
+ * registers every value it ingests"): the check pipeline passes
74
+ * (raw, digest) => sanitizer.register(digest, raw) so flagged literals are
75
+ * scrubbed from all rendered text; stays local so the import-audit holds.
76
+ */
77
+ readonly onMatch?: (rawValue: string, valueDigest: string) => void;
78
+ };
79
+
80
+ export type ExfilFinding = {
81
+ /** severity.ts-resolved id: bare rule on tree, `exfil-*:message` on messages. */
82
+ readonly id: string;
83
+ readonly rule: ExfilRuleId;
84
+ readonly severity: ExfilSeverity;
85
+ readonly facet: ExfilFacet;
86
+ readonly channel: ExfilChannel;
87
+ /** parity with Finding.engine: string (src/findings.ts); native legs stamp EXFIL_ENGINE. */
88
+ readonly engine: string;
89
+ readonly message: string;
90
+ readonly source: string;
91
+ readonly line: number;
92
+ readonly valueDigest: string;
93
+ readonly snippet: string;
94
+ };
95
+
96
+ const PREDICATES: Readonly<Record<ExfilRuleId, (text: string, o: RuleOptions) => RuleHit[]>> = {
97
+ "exfil-rfc1918": matchRfc1918,
98
+ "exfil-ssh-target": matchSshTarget,
99
+ "exfil-home-path": matchHomePath,
100
+ "exfil-cred-location": matchCredLocation,
101
+ "exfil-host-profile": matchHostProfile,
102
+ };
103
+
104
+ /**
105
+ * One emitter for both facets: iterate exactly the rules the matrix ALLOWS for
106
+ * (facet × native), stamp severity from the same lookup (single source, no
107
+ * re-decision), and mask the evidence. Deterministic order: line, then id,
108
+ * then digest.
109
+ */
110
+ function scanFacet(input: ExfilScanInput, facet: Extract<ExfilFacet, "tree" | "message">): ExfilFinding[] {
111
+ const channel: ExfilChannel = "native";
112
+ const options: RuleOptions = { facet, ...(input.hosts !== undefined ? { hosts: input.hosts } : {}) };
113
+ const findings: ExfilFinding[] = [];
114
+ for (const rule of emitRulesFor(facet, channel)) {
115
+ const severity = observedSeverity(rule, facet, channel);
116
+ if (severity === null) continue; // emitRulesFor excludes nulls; type narrowing kept honest.
117
+ for (const hit of PREDICATES[rule](input.text, options)) {
118
+ const { valueDigest, snippet } = maskValue(hit.matched);
119
+ if (EXFIL_SECRET_VALUE_RULES.includes(rule)) input.onMatch?.(hit.matched, valueDigest); // F4: location class never registers
120
+ findings.push({
121
+ id: exfilFindingId(rule, facet),
122
+ rule,
123
+ severity,
124
+ facet,
125
+ channel,
126
+ engine: EXFIL_ENGINE,
127
+ message: hit.message,
128
+ source: input.source,
129
+ line: hit.line,
130
+ valueDigest,
131
+ snippet,
132
+ });
133
+ }
134
+ }
135
+ return findings.sort((a, b) => a.line - b.line || a.id.localeCompare(b.id) || a.valueDigest.localeCompare(b.valueDigest));
136
+ }
137
+
138
+ /** Native tree/blob leg: MEDIUM-family rules only, per the matrix (HIGH family = twins). */
139
+ export function scanTreeText(input: ExfilScanInput): ExfilFinding[] {
140
+ return scanFacet(input, "tree");
141
+ }
142
+
143
+ /** Commit-message leg: all five rules under the `:message` id family, sha attribution. */
144
+ export function scanMessageText(input: ExfilScanInput): ExfilFinding[] {
145
+ return scanFacet(input, "message");
146
+ }
@@ -0,0 +1,156 @@
1
+ // provenance: .omo/plans/border-exfil-lens.md T1 — the single home of the
2
+ // (rule × facet × channel → observed severity) matrix, plan §"分面契约" (R2).
3
+ //
4
+ // THE MATRIX IS THE PRODUCT DECISION and lives here and nowhere else; the
5
+ // later README rule table (T8) and the check-pipeline stages (T2/T3) render
6
+ // this table, they never re-decide it. Cell semantics:
7
+ //
8
+ // * tree facet, twin channels — the HIGH family (exfil-rfc1918,
9
+ // exfil-ssh-target) rides the vendored gitleaks TOML + secretlint border
10
+ // patterns (T2 ships them with the ids VERBATIM equal to these rule ids).
11
+ // Observed severity is CRITICAL, not the table HIGH, because that is the
12
+ // channels' mechanical truth: gitleaks.ts:81 hardcodes severity
13
+ // "CRITICAL" for every finding it ingests, and secretlint.ts severityOf
14
+ // (299-309) maps error→CRITICAL with NO HIGH rung ("不硬掰,如实表").
15
+ // * tree facet, native channel — the MEDIUM family (exfil-home-path,
16
+ // exfil-cred-location, exfil-host-profile) only. The v2 native tree/blob
17
+ // double-emission of the HIGH family was REVOKED in plan REV v3
18
+ // ("撤销 v2 的 native 树/blob 双发 stage"): source-level elimination of
19
+ // triple-firing. A null cell here is a FORBIDDEN emission, and the guard
20
+ // tests pin it.
21
+ // * message facet — native EXCLUSIVE (`exfil-*:message` id family, T3's
22
+ // messageScan leg): all five rules at their table severity. The twins do
23
+ // not scan commit messages; a (message, twin) cell is null.
24
+ // * tagNote facet — owned by the EXISTING engine leg: the tagScan stdin leg
25
+ // (tagScan.ts:26,101) rides the vendored TOML and re-labels every hit
26
+ // `tag-message-secret` CRITICAL (path = the tag refname). The core
27
+ // therefore NEVER scans tag notes and no exfil-* finding id may be
28
+ // observed on this facet — every tagNote cell is null, eliminating
29
+ // double-firing by construction. The `:message` family covers commit
30
+ // messages only.
31
+ //
32
+ // Isolation note (plan §分面契约): allow-list entries are matched EXACTLY on
33
+ // the rule id (allow.ts:38 → matchGlob with no wildcards in a plain id), so a
34
+ // blob-face exemption `{rule: "exfil-rfc1918"}` never pierces the
35
+ // `exfil-rfc1918:message` id and vice versa — the ids are kept structurally
36
+ // distinct HERE (exfilFindingId) so the two families can never collide.
37
+ //
38
+ // Import-audit (plan §分面契约 last bullet): this directory imports only node
39
+ // builtins and local exfil files — hence the severity union is re-declared
40
+ // here instead of importing src/findings.ts; T2's pipeline maps these cells
41
+ // onto Finding verbatim, and the guard test pins ordering compatibility.
42
+
43
+ /** Mirrors SEVERITIES in src/findings.ts (re-declared: exfil core stays import-free). */
44
+ export const EXFIL_SEVERITIES = ["INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL"] as const;
45
+ export type ExfilSeverity = (typeof EXFIL_SEVERITIES)[number];
46
+
47
+ /** The five exfil-lens rule ids, verbatim; twins must ship these ids byte-equal (T2 guard). */
48
+ export const EXFIL_RULE_IDS = [
49
+ "exfil-rfc1918",
50
+ "exfil-ssh-target",
51
+ "exfil-home-path",
52
+ "exfil-cred-location",
53
+ "exfil-host-profile",
54
+ ] as const;
55
+ export type ExfilRuleId = (typeof EXFIL_RULE_IDS)[number];
56
+
57
+ /** Message-facet attribution id family, e.g. "exfil-rfc1918:message" (plan §分面契约 row 2). */
58
+ export const EXFIL_MESSAGE_ID_SUFFIX = ":message";
59
+ export type ExfilMessageRuleId = `${ExfilRuleId}:message`;
60
+
61
+ export const EXFIL_FACETS = ["tree", "message", "tagNote"] as const;
62
+ export type ExfilFacet = (typeof EXFIL_FACETS)[number];
63
+
64
+ export const EXFIL_CHANNELS = ["native", "gitleaks-twin", "secretlint-twin"] as const;
65
+ export type ExfilChannel = (typeof EXFIL_CHANNELS)[number];
66
+
67
+ /** Intent severities from the plan's 规则语义表 (what the rule means, before channel mechanics). */
68
+ export const EXFIL_TABLE_SEVERITY: Readonly<Record<ExfilRuleId, ExfilSeverity>> = {
69
+ "exfil-rfc1918": "HIGH",
70
+ "exfil-ssh-target": "HIGH",
71
+ "exfil-home-path": "MEDIUM",
72
+ "exfil-cred-location": "MEDIUM",
73
+ "exfil-host-profile": "MEDIUM",
74
+ };
75
+
76
+ /** HIGH family: the two identity-carrying location rules; tree face belongs to the twins. */
77
+ export const EXFIL_HIGH_FAMILY: readonly ExfilRuleId[] = ["exfil-rfc1918", "exfil-ssh-target"];
78
+ /**
79
+ * Identifier class (F4): the matched text is a secret-shaped VALUE (an
80
+ * internal IP, a user@internal-host) — the check legs register these with the
81
+ * run's TextSanitizer so the literal cannot resurface in rendered output.
82
+ * The MEDIUM location class must NEVER register: there the matched text is a
83
+ * path whose literal printing IS the finding, and registering it turns every
84
+ * later render of that path into a [REDACTED storm (dogfood, round 2).
85
+ */
86
+ export const EXFIL_SECRET_VALUE_RULES: readonly ExfilRuleId[] = EXFIL_HIGH_FAMILY;
87
+ /** MEDIUM family: privacy-shape rules; native carries them on both scanned facets. */
88
+ export const EXFIL_MEDIUM_FAMILY: readonly ExfilRuleId[] = ["exfil-home-path", "exfil-cred-location", "exfil-host-profile"];
89
+
90
+ /** null = FORBIDDEN cell: that channel must never emit that rule on that facet. */
91
+ export type ObservedSeverity = ExfilSeverity | null;
92
+
93
+ const TWIN_HIGH: Readonly<Record<ExfilRuleId, ObservedSeverity>> = {
94
+ "exfil-rfc1918": "CRITICAL",
95
+ "exfil-ssh-target": "CRITICAL",
96
+ "exfil-home-path": null,
97
+ "exfil-cred-location": null,
98
+ "exfil-host-profile": null,
99
+ };
100
+ const NULL_ROW: Readonly<Record<ExfilRuleId, ObservedSeverity>> = {
101
+ "exfil-rfc1918": null,
102
+ "exfil-ssh-target": null,
103
+ "exfil-home-path": null,
104
+ "exfil-cred-location": null,
105
+ "exfil-host-profile": null,
106
+ };
107
+
108
+ /** The full (facet × channel × rule) truth table — the plan's 分面契约, machine-readable. */
109
+ export const EXFIL_MATRIX: Readonly<Record<ExfilFacet, Readonly<Record<ExfilChannel, Readonly<Record<ExfilRuleId, ObservedSeverity>>>>>> = {
110
+ tree: {
111
+ // native tree leg = MEDIUM family补发 only (v2 double-emission revoked).
112
+ native: { ...NULL_ROW, "exfil-home-path": "MEDIUM", "exfil-cred-location": "MEDIUM", "exfil-host-profile": "MEDIUM" },
113
+ // gitleaks.ts:81 hardcodes CRITICAL for every ingested finding.
114
+ "gitleaks-twin": TWIN_HIGH,
115
+ // secretlint.ts:299-309 has no HIGH rung — error maps to CRITICAL as-is.
116
+ "secretlint-twin": TWIN_HIGH,
117
+ },
118
+ message: {
119
+ // native-exclusive `:message` family at table severity.
120
+ native: { ...EXFIL_TABLE_SEVERITY },
121
+ "gitleaks-twin": NULL_ROW,
122
+ "secretlint-twin": NULL_ROW,
123
+ },
124
+ tagNote: {
125
+ // engine既有腿 owns this facet as `tag-message-secret` (a non-exfil id);
126
+ // NO exfil-* emission is legal on tag notes through ANY channel.
127
+ native: NULL_ROW,
128
+ "gitleaks-twin": NULL_ROW,
129
+ "secretlint-twin": NULL_ROW,
130
+ },
131
+ };
132
+
133
+ /** Observed severity for one (rule × facet × channel) cell; null = that emission is forbidden. */
134
+ export function observedSeverity(rule: ExfilRuleId, facet: ExfilFacet, channel: ExfilChannel): ObservedSeverity {
135
+ return EXFIL_MATRIX[facet][channel][rule];
136
+ }
137
+
138
+ /** Every rule a (facet × channel) combination is ALLOWED to emit, in EXFIL_RULE_IDS order. */
139
+ export function emitRulesFor(facet: ExfilFacet, channel: ExfilChannel): readonly ExfilRuleId[] {
140
+ return EXFIL_RULE_IDS.filter((rule) => observedSeverity(rule, facet, channel) !== null);
141
+ }
142
+
143
+ /**
144
+ * The finding id for a (rule × facet) pair: the `:message` family ONLY on the
145
+ * message facet; tree/tagNote carry the bare rule id (the id must stay
146
+ * exactly "exfil-*" for the twin-id-equality and blob-face allow-list match).
147
+ */
148
+ export function exfilFindingId(rule: ExfilRuleId, facet: ExfilFacet): string {
149
+ switch (facet) {
150
+ case "message":
151
+ return `${rule}${EXFIL_MESSAGE_ID_SUFFIX}`;
152
+ case "tree":
153
+ case "tagNote":
154
+ return rule;
155
+ }
156
+ }
@@ -3207,3 +3207,53 @@ description = "Detected a Zendesk Secret Key, risking unauthorized access to cus
3207
3207
  regex = '''(?i)[\w.-]{0,50}?(?:zendesk)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)'''
3208
3208
  keywords = ["zendesk"]
3209
3209
 
3210
+ # ---------------------------------------------------------------------------
3211
+ # border vendored additions (NOT upstream-generated; plan
3212
+ # .omo/plans/border-exfil-lens.md T2 — exfil lens tree-facet HIGH family, the
3213
+ # "gitleaks-twin" channel). Ids must stay VERBATIM equal to src/exfil rule ids
3214
+ # (EXFIL_HIGH_FAMILY); the twin guard parses this file. Go RE2 has no
3215
+ # lookaround, so boundaries use NON-CAPTURING character classes and the token
3216
+ # itself rides (?P<secret>…): that is what gitleaks 8.30.1 digests as Secret
3217
+ # (border's adapter hashes Secret over Match) and what per-rule allowlists are
3218
+ # matched against by default. The adjacent-IP edge (second IP on one line
3219
+ # shadowed by the consumed separator) is covered by the secretlint twin's JS
3220
+ # lookarounds. No literal dotted quads appear here — octet classes only
3221
+ # (self-stealth).
3222
+ # F2 VERDICT PARITY: the per-rule allowlists cancel findings whose secret is
3223
+ # an exempt-band address — loopback, each RFC5737 /24, and the 10.200/16
3224
+ # synthetic-fixture band (mirrors src/exfil DEFAULT_EXEMPT_BANDS; corpus-pinned
3225
+ # by the VERDICT-PARITY test in test/exfil.pipeline.test.ts). Anchored ^ on the
3226
+ # secret; ssh secrets are "user@host", so their patterns anchor at the @.
3227
+ # ---------------------------------------------------------------------------
3228
+
3229
+ [[rules]]
3230
+ id = "exfil-rfc1918"
3231
+ description = "RFC1918 private-network address literal on a surface about to become public (border exfil lens)."
3232
+ regex = '''(?:^|[^0-9A-Za-z.])(?P<secret>10\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){2}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|172\.(?:1[6-9]|2[0-9]|3[01])\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.)(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|192\.168\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.)(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]))(?:[^0-9A-Za-z.]|$)'''
3233
+ keywords = ["10.", "172.", "192."]
3234
+ severity = "critical"
3235
+ [[rules.allowlists]]
3236
+ description = "border exfil F2 parity: exempt bands (127/8, RFC5737 /24s, 10.200/16 synthetic-fixture band)"
3237
+ regexes = [
3238
+ '''^127\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){2}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$''',
3239
+ '''^192\.0\.2\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$''',
3240
+ '''^198\.51\.100\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$''',
3241
+ '''^203\.0\.113\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$''',
3242
+ '''^10\.200\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.)(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$''',
3243
+ ]
3244
+
3245
+ [[rules]]
3246
+ id = "exfil-ssh-target"
3247
+ description = "SSH-style user@host naming an internal-only target: RFC1918 literal or private suffix (.internal/.lan/.local/.corp/.intra; RFC2606 shapes cannot match a suffix at word end)."
3248
+ regex = '''(?:^|[^0-9A-Za-z._+%-])(?P<secret>[0-9A-Za-z._+%-]+@(?:10\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){2}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|172\.(?:1[6-9]|2[0-9]|3[01])\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.)(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|192\.168\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.)(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|[0-9A-Za-z](?:[0-9A-Za-z-]*[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:[0-9A-Za-z-]*[0-9A-Za-z])?)*\.(?:internal|lan|local|corp|intra)))(?:[^0-9A-Za-z._-]|$)'''
3249
+ keywords = [".internal", ".lan", ".local", ".corp", ".intra", "10.", "172.", "192."]
3250
+ severity = "critical"
3251
+ [[rules.allowlists]]
3252
+ description = "border exfil F2 parity: exempt-band IP hosts never count as internal ssh targets on the tree facet (10.200/16 fixture band et al.)"
3253
+ regexes = [
3254
+ '''^[0-9A-Za-z._+%-]+@127\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){2}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$''',
3255
+ '''^[0-9A-Za-z._+%-]+@192\.0\.2\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$''',
3256
+ '''^[0-9A-Za-z._+%-]+@198\.51\.100\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$''',
3257
+ '''^[0-9A-Za-z._+%-]+@203\.0\.113\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$''',
3258
+ '''^[0-9A-Za-z._+%-]+@10\.200\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.)(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$''',
3259
+ ]
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "border-customs",
3
- "version": "0.5.1",
3
+ "version": "0.7.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "border-customs",
9
- "version": "0.5.1",
9
+ "version": "0.7.1",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@opencode-ai/plugin": "^1.17.4",