rowstile 0.1.0-alpha.2 → 0.1.0-alpha.3
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/command/authzlib/__init__.py +1 -1
- package/command/authzlib/base.py +2 -1
- package/command/authzlib/compiler.py +23 -8
- package/command/authzlib/devtools.py +16 -0
- package/command/authzlib/errors.py +4 -3
- package/command/authzlib/hardening.py +3 -1
- package/command/authzlib/output.py +66 -0
- package/command/cli/pgwire.py +1 -0
- package/package.json +8 -8
|
@@ -10,7 +10,7 @@ class Compiler(TestMixin, DevMixin, OutputMixin):
|
|
|
10
10
|
"""A policy compiled: each part of the output is one of its bases (OutputMixin assembles them)."""
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
__version__ = "0.1.0-alpha.
|
|
13
|
+
__version__ = "0.1.0-alpha.3" # set with packaging/version.py (the packages copy it); authz.policy_versions records it on each apply
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def _build() -> str:
|
package/command/authzlib/base.py
CHANGED
|
@@ -182,4 +182,5 @@ GENERATED_FUNCTIONS = ("act_as", "can", "list", "perms", "perms_of", "share", "u
|
|
|
182
182
|
"create_api_key", "list_api_keys", "revoke_api_key", "login_key", "login_jwt", "view_as",
|
|
183
183
|
"sync_members", "changes_since", "trim_changes", "trim_audit", "request_access", "pending_requests", "decide_request",
|
|
184
184
|
"cancel_request", "break_glass", "start_review", "review_items", "review_decide",
|
|
185
|
-
"close_review", "check_invariants", "lint", "connection_check", "explain_rule", "who_among"
|
|
185
|
+
"close_review", "check_invariants", "lint", "connection_check", "explain_rule", "who_among",
|
|
186
|
+
"list_links", "revoke_link")
|
|
@@ -135,7 +135,7 @@ def once(items: Sequence[Expr]) -> list[Expr]:
|
|
|
135
135
|
# for one read grows with every permission named more than once on the way (Core.expansions counts them).
|
|
136
136
|
VIEW_BODY = re.compile(r'CREATE VIEW authz_int\.("(?:[^"]|"")+"|\w+) AS\n(.*?);(?=\nCREATE VIEW authz_gen|\Z)', re.S)
|
|
137
137
|
VIEW_NAME = re.compile(r'authz_(?:int|gen)\.("(?:[^"]|"")+"|\w+)')
|
|
138
|
-
# the policies here stay under
|
|
138
|
+
# the policies here stay under 25; from about 70, planning a read can take a tenth of a second and more
|
|
139
139
|
MANY_EXPANSIONS = 50
|
|
140
140
|
|
|
141
141
|
|
|
@@ -1278,7 +1278,11 @@ class Core:
|
|
|
1278
1278
|
header = f"-- {t.name}.{perm.name} ({perm.loc}): {perm.src}\n"
|
|
1279
1279
|
key = self.recursive.get((t.name, perm.name))
|
|
1280
1280
|
if key is None:
|
|
1281
|
-
|
|
1281
|
+
# as the row checks do (row_sql): the permissions of its type written out in place, so a lookup
|
|
1282
|
+
# another one covers is dropped. view names parent.view and, through edit, parent.edit, which
|
|
1283
|
+
# parent.view includes: named as views, each would be written out again at every level above
|
|
1284
|
+
items = self.prune(t, self.flat_items(t, perm, views=True))
|
|
1285
|
+
sql = union([self.set_sql(t, i, perm.loc) for i in items])
|
|
1282
1286
|
else:
|
|
1283
1287
|
members = self.scc_members[key]
|
|
1284
1288
|
edges = self.scc_edges(key)
|
|
@@ -1476,14 +1480,17 @@ class Core:
|
|
|
1476
1480
|
return f"SELECT {rid} AS id FROM {tbl} r\n WHERE " + "\n AND ".join(where)
|
|
1477
1481
|
|
|
1478
1482
|
# --- policies: flatten same-type permissions, drop redundant lookups ---
|
|
1479
|
-
def flat_items(self, t: Type, perm: Perm, stack: tuple[Name, ...] = ()) -> list[Expr]:
|
|
1483
|
+
def flat_items(self, t: Type, perm: Perm, stack: tuple[Name, ...] = (), views: bool = False) -> list[Expr]:
|
|
1484
|
+
"""t.perm's operands, with the permissions of its type it names written out in place. views: for the
|
|
1485
|
+
permission's view, where one that inherits stays a name (its view walks a tree, which its operands
|
|
1486
|
+
written out here would not)."""
|
|
1480
1487
|
key = (t.name, perm.name)
|
|
1481
1488
|
if key in stack:
|
|
1482
1489
|
fail(perm.loc, f"{t.name}.{perm.name} depends on itself", "AZ302")
|
|
1483
1490
|
out = []
|
|
1484
1491
|
for item in self.top_items(perm):
|
|
1485
|
-
if isinstance(item, Ref) and item.name in t.perms:
|
|
1486
|
-
out += self.flat_items(t, t.perms[item.name], (*stack, key))
|
|
1492
|
+
if isinstance(item, Ref) and item.name in t.perms and not (views and (t.name, item.name) in self.recursive):
|
|
1493
|
+
out += self.flat_items(t, t.perms[item.name], (*stack, key), views)
|
|
1487
1494
|
else:
|
|
1488
1495
|
out.append(item)
|
|
1489
1496
|
return out
|
|
@@ -1507,11 +1514,14 @@ class Core:
|
|
|
1507
1514
|
conds = [x for x in item.items if isinstance(x, Cond)]
|
|
1508
1515
|
if len(arrows) == 1 and len(conds) == 1:
|
|
1509
1516
|
item, cond = arrows[0], conds[0].sql
|
|
1510
|
-
if isinstance(item, Arrow) and item.rel in t.relations
|
|
1517
|
+
if (isinstance(item, Arrow) and item.rel in t.relations
|
|
1518
|
+
and all(not sr for _, sr in t.relations[item.rel].subjects())):
|
|
1511
1519
|
return (item.rel, item.perm, cond)
|
|
1512
1520
|
return None
|
|
1513
1521
|
|
|
1514
1522
|
def prune(self, t: Type, items: list[Expr]) -> list[Expr]:
|
|
1523
|
+
"""items (the operands of an or) without those said twice, and without a lookup another one covers:
|
|
1524
|
+
parent.edit beside parent.view, when view includes edit on every type parent points at (reach)."""
|
|
1515
1525
|
keep = []
|
|
1516
1526
|
for i, item in enumerate(items):
|
|
1517
1527
|
if item in items[:i]:
|
|
@@ -1519,12 +1529,17 @@ class Core:
|
|
|
1519
1529
|
ki = self.lookup_key(t, item)
|
|
1520
1530
|
covered = False
|
|
1521
1531
|
if ki:
|
|
1522
|
-
|
|
1532
|
+
targets = [self.T(st) for st, _ in t.relations[ki[0]].subjects()]
|
|
1533
|
+
|
|
1534
|
+
def within(inner: str, outer: str, targets: list[Type] = targets) -> bool:
|
|
1535
|
+
return bool(targets) and all(inner in self.reach(target, outer) for target in targets)
|
|
1536
|
+
|
|
1523
1537
|
for j, other in enumerate(items):
|
|
1524
1538
|
kj = self.lookup_key(t, other)
|
|
1525
1539
|
if j == i or not kj or kj[0] != ki[0] or kj[2] != ki[2]:
|
|
1526
1540
|
continue
|
|
1527
|
-
|
|
1541
|
+
# (two that include each other say the same: the first stays)
|
|
1542
|
+
if within(ki[1], kj[1]) and not (within(kj[1], ki[1]) and j > i):
|
|
1528
1543
|
covered = True
|
|
1529
1544
|
break
|
|
1530
1545
|
if not covered:
|
|
@@ -252,6 +252,13 @@ export function authz(db: Queryable) {{
|
|
|
252
252
|
/** A share with anyone holding the returned token (shown once; only its hash is stored). */
|
|
253
253
|
createLink: async <T extends SharedType>(type: T, id: Id, relation: SharedRelation<T>, expiresAt?: Date): Promise<string> =>
|
|
254
254
|
(await db.query("SELECT authz.create_link($1, $2::text, $3, $4) AS token", [type, s(id), relation, expiresAt ?? null])).rows[0].token,
|
|
255
|
+
/** The links on an object (id, relation, created_by, created_at, expires_at), for those who can share it or may
|
|
256
|
+
* make such links. */
|
|
257
|
+
listLinks: async <T extends ObjectType>(type: T, id: Id): Promise<any[]> =>
|
|
258
|
+
(await db.query("SELECT * FROM authz.list_links($1, $2::text)", [type, s(id)])).rows,
|
|
259
|
+
/** Turns a link off, by its id in listLinks (not its token). */
|
|
260
|
+
revokeLink: async <T extends ObjectType>(type: T, id: Id, linkId: string) =>
|
|
261
|
+
db.query("SELECT authz.revoke_link($1, $2::text, $3)", [type, s(id), linkId]),
|
|
255
262
|
/** Link tokens the request presents, for this transaction. */
|
|
256
263
|
useLinks: async (tokens: string[]) => db.query("SELECT set_config('authz_ctx.links', $1, true)", [tokens.join(",")]),
|
|
257
264
|
cancelRequest: async (requestId: Id) => db.query("SELECT authz.cancel_request($1::bigint)", [s(requestId)]),
|
|
@@ -537,6 +544,15 @@ class Authz:
|
|
|
537
544
|
raise ValueError(f"{{type_}}.{{relation}} is not shared in the policy")
|
|
538
545
|
return str(self._one("SELECT authz.create_link(%s, %s::text, %s, %s)", (type_, _id(id_), relation, expires_at)))
|
|
539
546
|
|
|
547
|
+
def list_links(self, type_: ObjectType, id_: Id) -> Sequence[Row]:
|
|
548
|
+
"""The links on an object (id, relation, created_by, created_at, expires_at), for those who can share it
|
|
549
|
+
or may make such links."""
|
|
550
|
+
return self._rows("SELECT * FROM authz.list_links(%s, %s::text)", (type_, _id(id_)))
|
|
551
|
+
|
|
552
|
+
def revoke_link(self, type_: ObjectType, id_: Id, link_id: str) -> None:
|
|
553
|
+
"""Turns a link off, by its id in list_links (not its token)."""
|
|
554
|
+
self._one("SELECT authz.revoke_link(%s, %s::text, %s)", (type_, _id(id_), link_id))
|
|
555
|
+
|
|
540
556
|
def use_links(self, tokens: Sequence[str]) -> None:
|
|
541
557
|
"""Link tokens the request presents, for this transaction (SET LOCAL authz_ctx.links)."""
|
|
542
558
|
self._one("SELECT set_config('authz_ctx.links', %s, true)", (",".join(tokens),))
|
|
@@ -553,8 +553,9 @@ CODES = {
|
|
|
553
553
|
"AZ705": Code(
|
|
554
554
|
"Not allowed",
|
|
555
555
|
"The signed-in person asked an `authz.*` function for something the policy doesn't let them do: share or "
|
|
556
|
-
"unshare this object, give a role, manage roles or keys, see who has access or the
|
|
557
|
-
"or a review, break the glass. The message says what;
|
|
556
|
+
"unshare this object, give a role, manage roles or keys, see who has access, the shares or the links, turn "
|
|
557
|
+
"a link off, decide a request or a review, break the glass. The message says what; "
|
|
558
|
+
"`authz.explain(type, id, perm)` says why. Answer 403.",
|
|
558
559
|
when="runtime"),
|
|
559
560
|
"AZ706": Code(
|
|
560
561
|
"The policy doesn't allow this share",
|
|
@@ -571,7 +572,7 @@ CODES = {
|
|
|
571
572
|
"AZ708": Code(
|
|
572
573
|
"No such thing",
|
|
573
574
|
"The call names something that isn't there: the subject to share with, an active user to view as, a "
|
|
574
|
-
"pending request, an item of a review, an API key of yours. Answer 404.",
|
|
575
|
+
"pending request, an item of a review, an API key of yours, a link on the object. Answer 404.",
|
|
575
576
|
when="runtime"),
|
|
576
577
|
"AZ709": Code(
|
|
577
578
|
"A write refused by a rule",
|
|
@@ -80,6 +80,7 @@ SELECT p.oid::regprocedure::text, gw.who, a.privilege_type,
|
|
|
80
80
|
"authz.unshare(text, bigint, text, text, bigint, text)",
|
|
81
81
|
"authz.unshare(text, bigint, text, text, text, text)",
|
|
82
82
|
"authz.create_link(text, text, text, timestamptz)", "authz.create_link(text, bigint, text, timestamptz)",
|
|
83
|
+
"authz.list_links(text, text)", "authz.revoke_link(text, text, text)",
|
|
83
84
|
"authz.create_role(text, text, text, text, text[])", "authz.create_role(text, bigint, text, text, text[])",
|
|
84
85
|
"authz.set_role_permissions(bigint, text[])", "authz.delete_role(bigint)", "authz.roles_of(text, text)",
|
|
85
86
|
"authz.roles_of(text, bigint)",
|
|
@@ -97,7 +98,8 @@ SELECT p.oid::regprocedure::text, gw.who, a.privilege_type,
|
|
|
97
98
|
"authz.review_items(bigint)",
|
|
98
99
|
"authz.review_decide(bigint, integer, boolean)", "authz.close_review(bigint, boolean)"]
|
|
99
100
|
executes += [f"authz.{fn}(text, {pt}{args})" for pt in sorted({t.pktype for t in typed})
|
|
100
|
-
for fn, args in (("who", ", text"), ("explain", ", text, text"), ("list_shares", ""))
|
|
101
|
+
for fn, args in (("who", ", text"), ("explain", ", text, text"), ("list_shares", ""), ("list_links", ""),
|
|
102
|
+
("revoke_link", ", text"))]
|
|
101
103
|
executes += [f"authz.can(text, {pt}, text)" for pt in sorted({t.pktype for t in typed})]
|
|
102
104
|
executes += [f"authz.who_among(text, {pt}, text, text[], text)" for pt in sorted({t.pktype for t in typed})]
|
|
103
105
|
executes += [f"authz.perms(text, {pt})" for pt in sorted({t.pktype for t in typed})]
|
|
@@ -28,6 +28,8 @@ DEFINER = "SECURITY DEFINER SET search_path = pg_catalog, pg_temp"
|
|
|
28
28
|
# for functions that run the policy's own SQL: the path applying vetted (search_path_sql), as views and policies resolve it
|
|
29
29
|
DEFINER_FROM_CURRENT = "SECURITY DEFINER SET search_path FROM CURRENT"
|
|
30
30
|
PT = "pg_catalog.current_setting('authz.principal_type', true)" # the signed-in principal's type ('': a user)
|
|
31
|
+
# a link's id, from its share g: the start of its token's hash (enough to name it among an object's links)
|
|
32
|
+
LINK_ID = "left(g.subject_id, 16)"
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
def subject_key(st: str, sr: str | None) -> str:
|
|
@@ -395,6 +397,13 @@ END $f$;
|
|
|
395
397
|
CREATE TRIGGER authz_role_gone AFTER DELETE ON authz.roles
|
|
396
398
|
REFERENCING OLD TABLE AS old_rows FOR EACH STATEMENT EXECUTE FUNCTION authz_int.role_gone();"""
|
|
397
399
|
|
|
400
|
+
link_columns = "id text, relation text, created_by text, created_at timestamptz, expires_at timestamptz"
|
|
401
|
+
link_overloads = "\n".join(
|
|
402
|
+
f"CREATE FUNCTION authz.list_links(p_type text, p_id {pt}) RETURNS TABLE ({link_columns}) LANGUAGE sql STABLE AS\n"
|
|
403
|
+
f" $$ SELECT * FROM authz.list_links(p_type, p_id::text) $$;\n"
|
|
404
|
+
f"CREATE FUNCTION authz.revoke_link(p_type text, p_id {pt}, p_link text) RETURNS void LANGUAGE sql AS\n"
|
|
405
|
+
f" $$ SELECT authz.revoke_link(p_type, p_id::text, p_link) $$;"
|
|
406
|
+
for pt in sorted({t.pktype for t in self.types.values()} - {"text"}))
|
|
398
407
|
api = f"""-- Share: needs the relation's 'shared by' permission on the object, and every permission
|
|
399
408
|
-- the relation grants (you can't grant more than you hold); the subject must exist
|
|
400
409
|
CREATE FUNCTION authz.share(p_type text, p_id text, p_relation text,
|
|
@@ -552,6 +561,63 @@ END $f$;
|
|
|
552
561
|
CREATE FUNCTION authz.create_link(p_type text, p_id bigint, p_relation text, p_expires_at timestamptz DEFAULT NULL)
|
|
553
562
|
RETURNS text LANGUAGE sql AS $$ SELECT authz.create_link(p_type, p_id::text, p_relation, p_expires_at) $$;
|
|
554
563
|
|
|
564
|
+
-- The relations whose links on an object the caller may see: every one it has links of if they may inspect it
|
|
565
|
+
-- (they can share it, or are an administrator), else those they may share with a link. NULL: none
|
|
566
|
+
CREATE FUNCTION authz_int.link_relations(p_type text, p_id text) RETURNS text[]
|
|
567
|
+
LANGUAGE plpgsql STABLE {DEFINER} AS $f$
|
|
568
|
+
BEGIN
|
|
569
|
+
IF authz_int.may_inspect(p_type, p_id) THEN
|
|
570
|
+
RETURN coalesce((SELECT array_agg(DISTINCT g.relation) FROM authz.shares g
|
|
571
|
+
WHERE g.object_type = p_type AND g.object_id = p_id AND g.subject_type = 'link'), '{{}}');
|
|
572
|
+
END IF;
|
|
573
|
+
RETURN (SELECT array_agg(s.relation) FROM authz_int.shared_relations s
|
|
574
|
+
WHERE s.object_type = p_type AND s.subject = 'link'
|
|
575
|
+
AND authz_int.may_manage(p_type, p_id, s.relation, 'link'));
|
|
576
|
+
END $f$;
|
|
577
|
+
-- The links on an object: SELECT * FROM authz.list_links('folder', 3). For those who can share it, or may make
|
|
578
|
+
-- such links. A link's id names it for authz.revoke_link: it is not the token, and opens nothing.
|
|
579
|
+
CREATE FUNCTION authz.list_links(p_type text, p_id text)
|
|
580
|
+
RETURNS TABLE (id text, relation text, created_by text, created_at timestamptz, expires_at timestamptz)
|
|
581
|
+
LANGUAGE plpgsql STABLE {DEFINER} AS $f$
|
|
582
|
+
DECLARE v_relations text[];
|
|
583
|
+
BEGIN
|
|
584
|
+
p_id := authz_int.canon(p_type, p_id);
|
|
585
|
+
v_relations := authz_int.link_relations(p_type, p_id);
|
|
586
|
+
IF v_relations IS NULL THEN
|
|
587
|
+
RAISE EXCEPTION 'you cannot see the links of % %', p_type, p_id USING ERRCODE = 'insufficient_privilege', HINT = 'rowstile help AZ705';
|
|
588
|
+
END IF;
|
|
589
|
+
RETURN QUERY SELECT {LINK_ID}, g.relation, g.created_by, g.created_at, g.expires_at
|
|
590
|
+
FROM authz.shares g WHERE g.object_type = p_type AND g.object_id = p_id AND g.subject_type = 'link'
|
|
591
|
+
AND g.relation = ANY (v_relations) ORDER BY g.created_at, g.subject_id;
|
|
592
|
+
END $f$;
|
|
593
|
+
-- Turn a link off, by its id in authz.list_links: needs what unsharing it needs
|
|
594
|
+
CREATE FUNCTION authz.revoke_link(p_type text, p_id text, p_link text) RETURNS void
|
|
595
|
+
LANGUAGE plpgsql {DEFINER} AS $f$
|
|
596
|
+
DECLARE v_relations text[]; v record; v_found boolean := false;
|
|
597
|
+
BEGIN
|
|
598
|
+
PERFORM authz_int.check_writable();
|
|
599
|
+
p_id := authz_int.canon(p_type, p_id);
|
|
600
|
+
v_relations := authz_int.link_relations(p_type, p_id);
|
|
601
|
+
IF v_relations IS NULL THEN
|
|
602
|
+
RAISE EXCEPTION 'you cannot turn off the links of % %', p_type, p_id USING ERRCODE = 'insufficient_privilege', HINT = 'rowstile help AZ705';
|
|
603
|
+
END IF;
|
|
604
|
+
FOR v IN SELECT g.relation, g.subject_id FROM authz.shares g
|
|
605
|
+
WHERE g.object_type = p_type AND g.object_id = p_id AND g.subject_type = 'link'
|
|
606
|
+
AND g.relation = ANY (v_relations) AND {LINK_ID} = p_link LOOP
|
|
607
|
+
IF NOT authz_int.may_manage(p_type, p_id, v.relation, 'link') THEN
|
|
608
|
+
RAISE EXCEPTION 'you cannot turn off link % of % % (you could not unshare it)', p_link, p_type, p_id
|
|
609
|
+
USING ERRCODE = 'insufficient_privilege', HINT = 'rowstile help AZ705';
|
|
610
|
+
END IF;
|
|
611
|
+
DELETE FROM authz.shares g WHERE g.object_type = p_type AND g.object_id = p_id AND g.relation = v.relation
|
|
612
|
+
AND g.subject_type = 'link' AND g.subject_id = v.subject_id;
|
|
613
|
+
v_found := true;
|
|
614
|
+
END LOOP;
|
|
615
|
+
IF NOT v_found THEN
|
|
616
|
+
RAISE EXCEPTION 'no link % on % %', p_link, p_type, p_id USING HINT = 'rowstile help AZ708';
|
|
617
|
+
END IF;
|
|
618
|
+
END $f$;
|
|
619
|
+
{link_overloads}
|
|
620
|
+
|
|
555
621
|
-- Custom roles: defined per owner (e.g. an org) by people with 'manage_roles' there
|
|
556
622
|
CREATE FUNCTION authz.create_role(p_owner_type text, p_owner_id text, p_object_type text, p_name text,
|
|
557
623
|
p_permissions text[]) RETURNS bigint LANGUAGE plpgsql {DEFINER} AS $f$
|
package/command/cli/pgwire.py
CHANGED
|
@@ -604,6 +604,7 @@ def _tls(sock: socket.socket, host: str, sslmode: str, sslrootcert: str | None)
|
|
|
604
604
|
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
605
605
|
context.check_hostname = False
|
|
606
606
|
context.verify_mode = ssl.CERT_NONE # encrypted, the server not checked: what require means
|
|
607
|
+
context.minimum_version = ssl.TLSVersion.TLSv1_2 # Python's default since 3.10, said for code scanners
|
|
607
608
|
try:
|
|
608
609
|
return context.wrap_socket(sock, server_hostname=host)
|
|
609
610
|
except ssl.SSLError as e:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rowstile",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.3",
|
|
4
4
|
"description": "Access rules for Postgres: the rowstile command (policies compiled to row-level security, shipped as migrations), with its own Python",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
"node": ">=18"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@rowstile/cli-linux-x64": "0.1.0-alpha.
|
|
24
|
-
"@rowstile/cli-linux-arm64": "0.1.0-alpha.
|
|
25
|
-
"@rowstile/cli-linux-x64-musl": "0.1.0-alpha.
|
|
26
|
-
"@rowstile/cli-linux-arm64-musl": "0.1.0-alpha.
|
|
27
|
-
"@rowstile/cli-darwin-x64": "0.1.0-alpha.
|
|
28
|
-
"@rowstile/cli-darwin-arm64": "0.1.0-alpha.
|
|
29
|
-
"@rowstile/cli-win32-x64": "0.1.0-alpha.
|
|
23
|
+
"@rowstile/cli-linux-x64": "0.1.0-alpha.3",
|
|
24
|
+
"@rowstile/cli-linux-arm64": "0.1.0-alpha.3",
|
|
25
|
+
"@rowstile/cli-linux-x64-musl": "0.1.0-alpha.3",
|
|
26
|
+
"@rowstile/cli-linux-arm64-musl": "0.1.0-alpha.3",
|
|
27
|
+
"@rowstile/cli-darwin-x64": "0.1.0-alpha.3",
|
|
28
|
+
"@rowstile/cli-darwin-arm64": "0.1.0-alpha.3",
|
|
29
|
+
"@rowstile/cli-win32-x64": "0.1.0-alpha.3"
|
|
30
30
|
}
|
|
31
31
|
}
|