msgctl 0.1.0a1__py3-none-any.whl
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.
- msg/__init__.py +2 -0
- msg/admin/__init__.py +0 -0
- msg/admin/backup_retirement.py +56 -0
- msg/admin/backups.py +341 -0
- msg/admin/custodial_check.py +77 -0
- msg/admin/diagnostics.py +750 -0
- msg/admin/market.py +91 -0
- msg/admin/market_check.py +341 -0
- msg/admin/money.py +322 -0
- msg/admin/preflight.py +142 -0
- msg/admin/recovery_replay.py +281 -0
- msg/admin/restore_database.py +37 -0
- msg/admin/root.py +347 -0
- msg/admin/rotation.py +197 -0
- msg/admin/token_delivery_check.py +54 -0
- msg/admin/upgrade_check.py +58 -0
- msg/application.py +163 -0
- msg/bootstrap.py +305 -0
- msg/cli.py +537 -0
- msg/client.py +725 -0
- msg/client_certificates.py +98 -0
- msg/client_content.py +43 -0
- msg/client_custodial.py +206 -0
- msg/client_market.py +104 -0
- msg/client_recovery.py +186 -0
- msg/client_secrets.py +58 -0
- msg/client_tokens.py +99 -0
- msg/client_upgrade.py +188 -0
- msg/config.py +318 -0
- msg/constants.py +11 -0
- msg/core/__init__.py +0 -0
- msg/core/batching.py +23 -0
- msg/core/codec.py +253 -0
- msg/core/contracts.py +128 -0
- msg/core/cursors.py +68 -0
- msg/core/email_address.py +33 -0
- msg/core/errors.py +22 -0
- msg/core/events.py +6 -0
- msg/core/execution_ports.py +20 -0
- msg/core/executor.py +193 -0
- msg/core/models.py +517 -0
- msg/core/packet.py +71 -0
- msg/core/permissions.py +14 -0
- msg/core/query.py +44 -0
- msg/core/read_query.py +60 -0
- msg/core/registry.py +173 -0
- msg/core/requests.py +58 -0
- msg/core/schema_policy.py +28 -0
- msg/core/schemas.py +30 -0
- msg/core/tags.py +21 -0
- msg/core/template_dsl.py +94 -0
- msg/core/text_patch.py +283 -0
- msg/core/tool_execution.py +28 -0
- msg/core/transfer.py +183 -0
- msg/daemon.py +255 -0
- msg/data/__init__.py +0 -0
- msg/data/bootstrap.json +72 -0
- msg/data/favicon.png +0 -0
- msg/data/logo-dark.svg +5 -0
- msg/data/logo.svg +5 -0
- msg/data/recovery-checkpoint.example.json +1 -0
- msg/data/recovery-checkpoint.schema.json +118 -0
- msg/data/shortcodes.json +1 -0
- msg/data/system/AGENTS.md +6 -0
- msg/data/system/rules/_index.md +17 -0
- msg/data/system/rules/auth.md +6 -0
- msg/data/system/rules/files.md +6 -0
- msg/data/system/rules/identity.md +6 -0
- msg/data/system/rules/protocol.md +6 -0
- msg/data/system/rules/read-write.md +6 -0
- msg/data/system/rules/recovery.md +6 -0
- msg/data/system/rules/security.md +10 -0
- msg/data/system/rules/topics.md +6 -0
- msg/extensions/__init__.py +1 -0
- msg/extensions/hosting.py +259 -0
- msg/extensions/keystore.py +96 -0
- msg/extensions/repositories.py +769 -0
- msg/extensions/rss.py +67 -0
- msg/extensions/ssh.py +264 -0
- msg/extensions/ssh_git.py +300 -0
- msg/extensions/tools.py +150 -0
- msg/hosting_runtime.py +138 -0
- msg/market/__init__.py +1 -0
- msg/market/arbitration.py +481 -0
- msg/market/delivery.py +338 -0
- msg/market/delivery_notifications.py +110 -0
- msg/market/delivery_targets.py +76 -0
- msg/market/email.py +100 -0
- msg/market/escrow.py +345 -0
- msg/market/orders.py +218 -0
- msg/market/policy.py +147 -0
- msg/market/rationale.py +91 -0
- msg/market/references.py +19 -0
- msg/market/targets.py +157 -0
- msg/plugins/__init__.py +28 -0
- msg/plugins/achievements.py +316 -0
- msg/plugins/batch.py +32 -0
- msg/plugins/bounty.py +366 -0
- msg/plugins/collaboration.py +261 -0
- msg/plugins/common.py +217 -0
- msg/plugins/communication.py +853 -0
- msg/plugins/content.py +855 -0
- msg/plugins/custodial_lifecycle.py +224 -0
- msg/plugins/delivery.py +230 -0
- msg/plugins/discovery.py +1266 -0
- msg/plugins/discussion.py +162 -0
- msg/plugins/extensions.py +10 -0
- msg/plugins/following.py +78 -0
- msg/plugins/hosting_capacity.py +52 -0
- msg/plugins/identity.py +1654 -0
- msg/plugins/money.py +221 -0
- msg/plugins/offers.py +183 -0
- msg/plugins/orders.py +273 -0
- msg/plugins/recovery.py +376 -0
- msg/plugins/schemas.py +5 -0
- msg/plugins/sharing.py +300 -0
- msg/plugins/store.py +249 -0
- msg/plugins/system.py +75 -0
- msg/plugins/transfer.py +249 -0
- msg/py.typed +0 -0
- msg/security/__init__.py +0 -0
- msg/security/age_keys.py +112 -0
- msg/security/authentication.py +141 -0
- msg/security/authorization.py +300 -0
- msg/security/backup_retirement.py +135 -0
- msg/security/capabilities.py +157 -0
- msg/security/certificates.py +183 -0
- msg/security/crypto.py +102 -0
- msg/security/custodial_migration.py +292 -0
- msg/security/custody_history.py +146 -0
- msg/security/network.py +81 -0
- msg/security/policy.py +67 -0
- msg/security/quarantine.py +19 -0
- msg/security/root_files.py +67 -0
- msg/security/rotation_journal.py +60 -0
- msg/security/sealed_box.py +70 -0
- msg/security/sharing_policy.py +29 -0
- msg/security/token_delivery.py +93 -0
- msg/security/vault.py +186 -0
- msg/storage/__init__.py +0 -0
- msg/storage/capacity.py +100 -0
- msg/storage/custodial_migration.py +22 -0
- msg/storage/git.py +435 -0
- msg/storage/ledger_migration.py +307 -0
- msg/storage/market_migration.py +49 -0
- msg/storage/postgres.py +663 -0
- msg/storage/query.py +39 -0
- msg/storage/read_only.py +126 -0
- msg/storage/session.py +403 -0
- msg/storage/sqlite.py +360 -0
- msg/storage/topic_event_migration.py +26 -0
- msg/storage/valkey_bus.py +45 -0
- msg/transports/__init__.py +0 -0
- msg/transports/client.py +183 -0
- msg/transports/dictionary.py +387 -0
- msg/transports/graphql.py +75 -0
- msg/transports/http.py +72 -0
- msg/transports/http_routes.py +1577 -0
- msg/transports/mcp.py +127 -0
- msg/transports/packet.py +42 -0
- msg/transports/read_tree_path.py +65 -0
- msg/transports/stdio.py +36 -0
- msg/transports/url_safety.py +123 -0
- msg/tui.py +368 -0
- msg/workers/__init__.py +1 -0
- msg/workers/effects.py +406 -0
- msg/workers/leases.py +19 -0
- msg/workers/mail.py +63 -0
- msg/workers/maintenance.py +310 -0
- msg/workers/sandbox.py +74 -0
- msg/workers/sandbox_child.py +189 -0
- msg/workers/webhook.py +160 -0
- msgctl-0.1.0a1.dist-info/METADATA +96 -0
- msgctl-0.1.0a1.dist-info/RECORD +176 -0
- msgctl-0.1.0a1.dist-info/WHEEL +4 -0
- msgctl-0.1.0a1.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""Transactional migration from inert escrow identities to typed ledger accounts.
|
|
2
|
+
|
|
3
|
+
Every stage runs on the caller's connection under the schema advisory lock.
|
|
4
|
+
This module never commits and never edits a historical ledger or signed receipt.
|
|
5
|
+
Reference paths describe protocol facts, not a heuristic scan of free text.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from psycopg import sql
|
|
11
|
+
|
|
12
|
+
_LEDGER_ACCOUNT_FKS = (
|
|
13
|
+
('money_accounts', 'money_accounts_subject_id_fkey',
|
|
14
|
+
'money_accounts_ledger_account_fkey', 'subject_id'),
|
|
15
|
+
('money_ledger', 'money_ledger_debit_account_fkey',
|
|
16
|
+
'money_ledger_debit_ledger_account_fkey', 'debit_account'),
|
|
17
|
+
('money_ledger', 'money_ledger_credit_account_fkey',
|
|
18
|
+
'money_ledger_credit_ledger_account_fkey', 'credit_account'),
|
|
19
|
+
('store_orders', 'store_orders_escrow_subject_fkey',
|
|
20
|
+
'store_orders_escrow_account_fkey', 'escrow_subject'),
|
|
21
|
+
('bounty_listings', 'bounty_listings_escrow_subject_fkey',
|
|
22
|
+
'bounty_listings_escrow_account_fkey', 'escrow_subject'),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _escrow_sources(conn):
|
|
27
|
+
escrow = {}
|
|
28
|
+
for table, kind, source in (('store_orders', 'order_escrow', 'id'),
|
|
29
|
+
('bounty_listings', 'bounty_escrow', 'listing_id'),
|
|
30
|
+
('money_purchases', 'purchase_escrow', 'id')):
|
|
31
|
+
column = 'escrow_account' if table == 'money_purchases' else 'escrow_subject'
|
|
32
|
+
for account_id, source_id in conn.execute(sql.SQL(
|
|
33
|
+
'SELECT {},{} FROM {}').format(
|
|
34
|
+
sql.Identifier(column), sql.Identifier(source), sql.Identifier(table))):
|
|
35
|
+
expected = (kind, None, source_id)
|
|
36
|
+
if account_id in escrow and escrow[account_id] != expected:
|
|
37
|
+
raise RuntimeError('ledger escrow account collision')
|
|
38
|
+
escrow[account_id] = expected
|
|
39
|
+
|
|
40
|
+
return escrow
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _install_accounts(conn, escrow):
|
|
44
|
+
ids = {row[0] for row in conn.execute('''SELECT subject_id FROM money_accounts
|
|
45
|
+
UNION SELECT debit_account FROM money_ledger WHERE debit_account IS NOT NULL
|
|
46
|
+
UNION SELECT credit_account FROM money_ledger WHERE credit_account IS NOT NULL''')}
|
|
47
|
+
ids.update(escrow)
|
|
48
|
+
for account_id in sorted(ids):
|
|
49
|
+
if account_id in escrow:
|
|
50
|
+
expected = escrow[account_id]
|
|
51
|
+
else:
|
|
52
|
+
identity = conn.execute('SELECT kind,body FROM identities WHERE id=%s',
|
|
53
|
+
(account_id,)).fetchone()
|
|
54
|
+
if identity is None or identity[0] != 'subject':
|
|
55
|
+
raise RuntimeError('ledger account has no subject or escrow source')
|
|
56
|
+
body = json.loads(identity[1])
|
|
57
|
+
if not ((account_id == 'u_root' and body.get('kind') == 'system' and
|
|
58
|
+
body.get('local_only') is True) or
|
|
59
|
+
(body.get('kind') in {'registered', 'custodial'} and
|
|
60
|
+
body.get('local_only') is not True)):
|
|
61
|
+
raise RuntimeError('invalid subject ledger account')
|
|
62
|
+
expected = ('subject', account_id, None)
|
|
63
|
+
conn.execute('''INSERT INTO ledger_accounts(id,kind,subject_id,source_id)
|
|
64
|
+
VALUES (%s,%s,%s,%s) ON CONFLICT(id) DO NOTHING''',
|
|
65
|
+
(account_id, *expected))
|
|
66
|
+
actual = conn.execute('''SELECT kind,subject_id,source_id FROM ledger_accounts
|
|
67
|
+
WHERE id=%s''', (account_id,)).fetchone()
|
|
68
|
+
if actual != expected:
|
|
69
|
+
raise RuntimeError('ledger account type/source mismatch')
|
|
70
|
+
for account_id, kind, subject_id, source_id in conn.execute('''
|
|
71
|
+
SELECT id,kind,subject_id,source_id FROM ledger_accounts
|
|
72
|
+
WHERE kind IN ('order_escrow','bounty_escrow','purchase_escrow')'''):
|
|
73
|
+
if escrow.get(account_id) != (kind, subject_id, source_id):
|
|
74
|
+
raise RuntimeError('orphan or mistyped escrow account')
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _validate_ledger(conn):
|
|
78
|
+
# All historical account IDs must now have typed LedgerAccount rows. The
|
|
79
|
+
# amount, sequence, receipt and account IDs in money_ledger stay untouched.
|
|
80
|
+
missing = conn.execute('''SELECT COUNT(*) FROM money_ledger l
|
|
81
|
+
LEFT JOIN ledger_accounts d ON d.id=l.debit_account
|
|
82
|
+
LEFT JOIN ledger_accounts c ON c.id=l.credit_account
|
|
83
|
+
WHERE (l.debit_account IS NOT NULL AND d.id IS NULL) OR
|
|
84
|
+
(l.credit_account IS NOT NULL AND c.id IS NULL)''').fetchone()[0]
|
|
85
|
+
if missing:
|
|
86
|
+
raise RuntimeError('unmapped historical ledger account')
|
|
87
|
+
supply = conn.execute("""SELECT COALESCE(SUM(CASE kind WHEN 'mint' THEN amount_minor
|
|
88
|
+
WHEN 'burn' THEN -amount_minor ELSE 0 END),0) FROM money_ledger""").fetchone()[0]
|
|
89
|
+
balances = conn.execute('''SELECT COALESCE(SUM(credit),0)-COALESCE(SUM(debit),0)
|
|
90
|
+
FROM (SELECT COALESCE(SUM(amount_minor),0) AS credit,0 AS debit
|
|
91
|
+
FROM money_ledger WHERE credit_account IS NOT NULL
|
|
92
|
+
UNION ALL
|
|
93
|
+
SELECT 0,COALESCE(SUM(amount_minor),0)
|
|
94
|
+
FROM money_ledger WHERE debit_account IS NOT NULL) b''').fetchone()[0]
|
|
95
|
+
if supply != balances:
|
|
96
|
+
raise RuntimeError('ledger conservation mismatch')
|
|
97
|
+
account_balances = conn.execute('''SELECT account_id,SUM(delta) FROM (
|
|
98
|
+
SELECT credit_account AS account_id,amount_minor AS delta FROM money_ledger
|
|
99
|
+
WHERE credit_account IS NOT NULL
|
|
100
|
+
UNION ALL
|
|
101
|
+
SELECT debit_account AS account_id,-amount_minor AS delta FROM money_ledger
|
|
102
|
+
WHERE debit_account IS NOT NULL) entries GROUP BY account_id''')
|
|
103
|
+
if any(balance < 0 or balance > 2**63-1 for _, balance in account_balances):
|
|
104
|
+
raise RuntimeError('invalid historical ledger balance')
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _validate_identity_references(conn, escrow):
|
|
108
|
+
# Refuse to delete a legacy escrow identity if it ever acquired identity,
|
|
109
|
+
# authority, credential or live resource references beyond the old FKs.
|
|
110
|
+
protected_names = {'subject', 'subject_id', 'owner', 'owner_subject',
|
|
111
|
+
'grantor', 'grantee', 'sender', 'recipient',
|
|
112
|
+
'recipient_subject', 'actor', 'claimant', 'holder',
|
|
113
|
+
'from_subject', 'to_subject', 'publisher', 'buyer', 'seller',
|
|
114
|
+
'parent', 'source_id', 'target_id', 'participant_a',
|
|
115
|
+
'participant_b', 'initiator', 'blocker', 'blocked',
|
|
116
|
+
'invited_by', 'granted_by', 'arbitrator', 'party', 'author'}
|
|
117
|
+
columns = [(table, column) for table, column in conn.execute('''
|
|
118
|
+
SELECT table_name,column_name FROM information_schema.columns
|
|
119
|
+
WHERE table_schema=current_schema()''')
|
|
120
|
+
if column in protected_names and table not in
|
|
121
|
+
{'money_accounts', 'ledger_accounts'}]
|
|
122
|
+
columns.extend((('resources', 'id'),))
|
|
123
|
+
legacy = set()
|
|
124
|
+
for account_id in escrow:
|
|
125
|
+
identity = conn.execute('SELECT kind,body FROM identities WHERE id=%s',
|
|
126
|
+
(account_id,)).fetchone()
|
|
127
|
+
if identity is None:
|
|
128
|
+
continue # A repeat startup after the migration.
|
|
129
|
+
legacy.add(account_id)
|
|
130
|
+
body = json.loads(identity[1])
|
|
131
|
+
if (identity[0] != 'subject' or body.get('kind') != 'system' or
|
|
132
|
+
body.get('local_only') is not True):
|
|
133
|
+
raise RuntimeError('legacy escrow identity is not inert')
|
|
134
|
+
for table, column in columns:
|
|
135
|
+
found = conn.execute(sql.SQL('SELECT 1 FROM {} WHERE {}=%s LIMIT 1').format(
|
|
136
|
+
sql.Identifier(table), sql.Identifier(column)), (account_id,)).fetchone()
|
|
137
|
+
if found:
|
|
138
|
+
raise RuntimeError('legacy escrow identity has active reference')
|
|
139
|
+
|
|
140
|
+
_validate_json_references(conn, legacy)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _switch_account_foreign_keys(conn):
|
|
144
|
+
for table, old_name, new_name, column in _LEDGER_ACCOUNT_FKS:
|
|
145
|
+
conn.execute(sql.SQL('ALTER TABLE {} DROP CONSTRAINT IF EXISTS {}').format(
|
|
146
|
+
sql.Identifier(table), sql.Identifier(old_name)))
|
|
147
|
+
exists = conn.execute('''SELECT 1 FROM pg_constraint WHERE conrelid=%s::regclass
|
|
148
|
+
AND conname=%s''', (table, new_name)).fetchone()
|
|
149
|
+
if not exists:
|
|
150
|
+
conn.execute(sql.SQL('ALTER TABLE {} ADD CONSTRAINT {} FOREIGN KEY ({}) '
|
|
151
|
+
'REFERENCES ledger_accounts(id)').format(
|
|
152
|
+
sql.Identifier(table), sql.Identifier(new_name),
|
|
153
|
+
sql.Identifier(column)))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _retire_legacy_identities(conn, escrow):
|
|
157
|
+
for account_id in escrow:
|
|
158
|
+
conn.execute('DELETE FROM identities WHERE id=%s AND kind=\'subject\'',
|
|
159
|
+
(account_id,))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _install_subject_account_trigger(conn):
|
|
163
|
+
conn.execute('''CREATE OR REPLACE FUNCTION msg_register_subject_ledger_account()
|
|
164
|
+
RETURNS trigger LANGUAGE plpgsql AS $$
|
|
165
|
+
DECLARE identity_body jsonb;
|
|
166
|
+
BEGIN
|
|
167
|
+
IF EXISTS (SELECT 1 FROM ledger_accounts WHERE id=NEW.subject_id) THEN
|
|
168
|
+
RETURN NEW;
|
|
169
|
+
END IF;
|
|
170
|
+
SELECT body::jsonb INTO identity_body FROM identities
|
|
171
|
+
WHERE id=NEW.subject_id AND kind='subject';
|
|
172
|
+
IF identity_body IS NULL OR NOT (
|
|
173
|
+
(NEW.subject_id='u_root' AND identity_body->>'kind'='system'
|
|
174
|
+
AND identity_body->>'local_only'='true') OR
|
|
175
|
+
(identity_body->>'kind' IN ('registered','custodial')
|
|
176
|
+
AND COALESCE(identity_body->>'local_only','false')='false')) THEN
|
|
177
|
+
RAISE EXCEPTION 'invalid_subject_ledger_account' USING ERRCODE='23514';
|
|
178
|
+
END IF;
|
|
179
|
+
INSERT INTO ledger_accounts(id,kind,subject_id,source_id)
|
|
180
|
+
VALUES (NEW.subject_id,'subject',NEW.subject_id,NULL);
|
|
181
|
+
RETURN NEW;
|
|
182
|
+
END $$''')
|
|
183
|
+
conn.execute('''DO $$ BEGIN
|
|
184
|
+
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname='money_account_register_subject'
|
|
185
|
+
AND tgrelid='money_accounts'::regclass) THEN
|
|
186
|
+
CREATE TRIGGER money_account_register_subject BEFORE INSERT ON money_accounts
|
|
187
|
+
FOR EACH ROW EXECUTE FUNCTION msg_register_subject_ledger_account();
|
|
188
|
+
END IF;
|
|
189
|
+
END $$''')
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# Paths are relative to the named JSON body. '*' traverses an array, never
|
|
193
|
+
# arbitrary dictionary keys. Event.data, result.data, change_note and content
|
|
194
|
+
# text are deliberately excluded: historical financial receipts legitimately
|
|
195
|
+
# contain escrow account IDs without granting those accounts an identity.
|
|
196
|
+
_GRANT_PATHS = ('grants.*.scope.resource_id', 'issuance.issue_grants.*.scope.resource_id',
|
|
197
|
+
'authority_sources.*.id')
|
|
198
|
+
_JSON_IDENTITY_REFS = {
|
|
199
|
+
'chunks': (), # TransferChunk contains a transfer ID and BlobRef, not an identity.
|
|
200
|
+
'resources': ('id', 'parent', 'owner', 'group', 'created_by', 'modified_by'),
|
|
201
|
+
'revisions': ('resource_id', 'actor', 'subject', 'author', 'relations.*.target.id'),
|
|
202
|
+
'relations': ('target.id',),
|
|
203
|
+
'identities': ('resource_id', 'primary_group'),
|
|
204
|
+
'memberships': ('organization_id', 'subject_id', 'invited_by'),
|
|
205
|
+
'emails': ('subject_id',),
|
|
206
|
+
'credentials': ('subject_id', 'ceiling.*.scope.resource_id'),
|
|
207
|
+
'certificates': ('resource_id', 'subject_id', 'issuer_id', 'parent_certificate_id',
|
|
208
|
+
*_GRANT_PATHS),
|
|
209
|
+
'csrs': ('resource_id', 'applicant', 'subject_id', 'requested_issuer', *_GRANT_PATHS),
|
|
210
|
+
'results': ('actor', 'subject', 'resources.*.id', 'output.id'),
|
|
211
|
+
'transfers': ('subject_id', 'target.id', 'output.id'),
|
|
212
|
+
'events': ('actor', 'subject', 'resources.*.id'),
|
|
213
|
+
'audit': ('event.actor', 'event.subject', 'event.resources.*.id', 'authority.*.id'),
|
|
214
|
+
'jobs': ('principal.subject', 'principal.actor', 'principal.certificates.*',
|
|
215
|
+
'principal.ceiling.*.scope.resource_id', 'result.id'),
|
|
216
|
+
'messages': ('sender', 'recipient', 'subject', 'resource.id'),
|
|
217
|
+
'reactions': ('subject', 'resource.id'),
|
|
218
|
+
'achievement_ceremonies': ('subject_id',),
|
|
219
|
+
'achievement_grants': ('subject_id', 'issuer'),
|
|
220
|
+
'handoffs': ('from_subject', 'to_subject', 'resources.*.id'),
|
|
221
|
+
'collaboration_leases': ('holder', 'target.id'),
|
|
222
|
+
'presence': ('subject',),
|
|
223
|
+
'claims': ('subject',),
|
|
224
|
+
'recovery_policies': ('subject', 'owner_subject'),
|
|
225
|
+
'recovery_envelopes': ('owner_subject', 'custodian_ref.id'),
|
|
226
|
+
'custodial_upgrades': ('subject', 'subject_id'),
|
|
227
|
+
'legacy_directive_versions': ('subject',),
|
|
228
|
+
'sync_checkpoints': ('subject',),
|
|
229
|
+
# Active market owners/panel authority are not financial account references.
|
|
230
|
+
# Settlement receipts retain historical debit/credit IDs intentionally.
|
|
231
|
+
'order_contracts': ('buyer', 'seller', 'buyer_principal.actor',
|
|
232
|
+
'buyer_principal.subject', 'buyer_principal.certificates.*',
|
|
233
|
+
'buyer_principal.ceiling.*.scope.resource_id',
|
|
234
|
+
'policy.policy.candidates.*'),
|
|
235
|
+
'order_settlements': (),
|
|
236
|
+
'order_transitions': ('actor',),
|
|
237
|
+
'delivery_envelopes': ('recipient_subject',),
|
|
238
|
+
'arbitration_policies': ('candidates.*',),
|
|
239
|
+
'arbitration_cases': ('buyer', 'seller', 'opened_by', 'policy.policy.candidates.*',
|
|
240
|
+
'panels.0.*', 'panels.1.*'),
|
|
241
|
+
'arbitration_votes': ('arbitrator', 'principal.actor', 'principal.subject',
|
|
242
|
+
'principal.certificates.*', 'principal.ceiling.*.scope.resource_id'),
|
|
243
|
+
'arbitration_decisions': ('voters.*',),
|
|
244
|
+
'arbitration_evidence': ('source.id',),
|
|
245
|
+
# Signed settlement parties are identities; order/account digests are not.
|
|
246
|
+
'order_escrow_decisions': ('actor', 'recipient'),
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _values_at(value, parts):
|
|
251
|
+
if not parts:
|
|
252
|
+
yield value
|
|
253
|
+
elif parts[0] == '*':
|
|
254
|
+
if isinstance(value, list):
|
|
255
|
+
for item in value:
|
|
256
|
+
yield from _values_at(item, parts[1:])
|
|
257
|
+
elif isinstance(value, dict) and parts[0] in value:
|
|
258
|
+
yield from _values_at(value[parts[0]], parts[1:])
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _validate_json_references(conn, legacy):
|
|
262
|
+
if not legacy:
|
|
263
|
+
return
|
|
264
|
+
body_tables = {row[0] for row in conn.execute("""SELECT table_name
|
|
265
|
+
FROM information_schema.columns WHERE table_schema=current_schema()
|
|
266
|
+
AND column_name='body'""")}
|
|
267
|
+
if body_tables - _JSON_IDENTITY_REFS.keys():
|
|
268
|
+
raise RuntimeError('unclassified legacy reference table')
|
|
269
|
+
for table, paths in _JSON_IDENTITY_REFS.items():
|
|
270
|
+
if table not in body_tables:
|
|
271
|
+
continue
|
|
272
|
+
query = sql.SQL('SELECT body FROM {}').format(sql.Identifier(table))
|
|
273
|
+
# These two self fields are the exact inert identities being retired;
|
|
274
|
+
# references to them from any OTHER identity are still unsafe.
|
|
275
|
+
parameters = None
|
|
276
|
+
if table == 'identities':
|
|
277
|
+
query += sql.SQL(' WHERE NOT (id=ANY(%s))')
|
|
278
|
+
parameters = (sorted(legacy),)
|
|
279
|
+
# A named cursor bounds client memory while reading retained history.
|
|
280
|
+
# It is scoped to this migration transaction, never a persistent cursor.
|
|
281
|
+
with conn.cursor(name='msg_legacy_refs_' + table) as rows:
|
|
282
|
+
rows.itersize = 256
|
|
283
|
+
rows.execute(query, parameters)
|
|
284
|
+
for (raw,) in rows:
|
|
285
|
+
try:
|
|
286
|
+
value = json.loads(raw)
|
|
287
|
+
except (TypeError, ValueError):
|
|
288
|
+
raise RuntimeError('invalid legacy reference document') from None
|
|
289
|
+
if not isinstance(value, dict):
|
|
290
|
+
raise RuntimeError('invalid legacy reference document')
|
|
291
|
+
for path in paths:
|
|
292
|
+
if any(isinstance(ref, str) and ref in legacy
|
|
293
|
+
for ref in _values_at(value, path.split('.'))):
|
|
294
|
+
# Never include a private body or arbitrary field value.
|
|
295
|
+
raise RuntimeError('legacy escrow identity has active reference')
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def migrate_ledger_accounts(conn):
|
|
300
|
+
"""All-or-nothing stages; fault injection belongs in tests, not config."""
|
|
301
|
+
escrow = _escrow_sources(conn)
|
|
302
|
+
_install_accounts(conn, escrow)
|
|
303
|
+
_validate_ledger(conn)
|
|
304
|
+
_validate_identity_references(conn, escrow)
|
|
305
|
+
_switch_account_foreign_keys(conn)
|
|
306
|
+
_retire_legacy_identities(conn, escrow)
|
|
307
|
+
_install_subject_account_trigger(conn)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Add named ledger legs without rewriting any historical entry or receipt.
|
|
2
|
+
|
|
3
|
+
The caller owns the schema lock and transaction. Repeated startup is safe.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
def migrate_market(conn):
|
|
7
|
+
conn.execute("ALTER TABLE money_ledger ADD COLUMN IF NOT EXISTS entry_key TEXT NOT NULL DEFAULT 'primary'")
|
|
8
|
+
conn.execute('ALTER TABLE money_ledger DROP CONSTRAINT IF EXISTS money_ledger_actor_request_id_key')
|
|
9
|
+
conn.execute('CREATE UNIQUE INDEX IF NOT EXISTS money_ledger_request_leg ON money_ledger(actor,request_id,entry_key)')
|
|
10
|
+
conn.execute('ALTER TABLE ledger_accounts DROP CONSTRAINT IF EXISTS ledger_accounts_kind_check')
|
|
11
|
+
conn.execute('ALTER TABLE ledger_accounts DROP CONSTRAINT IF EXISTS ledger_accounts_check')
|
|
12
|
+
conn.execute("""ALTER TABLE ledger_accounts ADD CONSTRAINT ledger_accounts_kind_check
|
|
13
|
+
CHECK(kind IN ('subject','order_escrow','bounty_escrow','purchase_escrow'))""")
|
|
14
|
+
conn.execute("""ALTER TABLE ledger_accounts ADD CONSTRAINT ledger_accounts_check
|
|
15
|
+
CHECK((kind='subject' AND subject_id=id AND source_id IS NULL) OR
|
|
16
|
+
(kind IN ('order_escrow','bounty_escrow','purchase_escrow') AND subject_id IS NULL AND source_id IS NOT NULL))""")
|
|
17
|
+
conn.execute('''CREATE TABLE IF NOT EXISTS money_purchases (
|
|
18
|
+
id TEXT PRIMARY KEY, subject_id TEXT NOT NULL REFERENCES identities(id),
|
|
19
|
+
request_id TEXT NOT NULL, offer_snapshot TEXT NOT NULL,
|
|
20
|
+
quantity BIGINT NOT NULL CHECK(quantity>0), total_minor BIGINT NOT NULL CHECK(total_minor>0),
|
|
21
|
+
escrow_account TEXT UNIQUE NOT NULL REFERENCES ledger_accounts(id),
|
|
22
|
+
state TEXT NOT NULL CHECK(state IN ('pending','settled','refunded')),
|
|
23
|
+
created_at TEXT NOT NULL, expires_at TEXT NOT NULL,
|
|
24
|
+
funding_transaction_id TEXT NOT NULL REFERENCES money_ledger(id),
|
|
25
|
+
final_transaction_id TEXT REFERENCES money_ledger(id),
|
|
26
|
+
entitlement_id TEXT REFERENCES resource_entitlements(id),
|
|
27
|
+
reason TEXT, UNIQUE(subject_id,request_id))''')
|
|
28
|
+
conn.execute('ALTER TABLE store_orders ALTER COLUMN payment_transaction_id DROP NOT NULL')
|
|
29
|
+
conn.execute('ALTER TABLE store_orders ALTER COLUMN funded_at DROP NOT NULL')
|
|
30
|
+
conn.execute("ALTER TABLE store_orders ADD COLUMN IF NOT EXISTS delivery_mode TEXT NOT NULL DEFAULT 'managed_instant'")
|
|
31
|
+
conn.execute('ALTER TABLE store_orders ADD COLUMN IF NOT EXISTS deadline_at TEXT')
|
|
32
|
+
conn.execute('ALTER TABLE store_orders ADD COLUMN IF NOT EXISTS quote_digest TEXT')
|
|
33
|
+
conn.execute('ALTER TABLE store_orders DROP CONSTRAINT IF EXISTS store_orders_state_check')
|
|
34
|
+
conn.execute("""ALTER TABLE store_orders ADD CONSTRAINT store_orders_state_check
|
|
35
|
+
CHECK(state IN ('created','funded','delivered','accepted','settled','cancelled','refunded','disputed'))""")
|
|
36
|
+
conn.execute('ALTER TABLE store_deliveries ALTER COLUMN package_digest DROP NOT NULL')
|
|
37
|
+
conn.execute('''CREATE TABLE IF NOT EXISTS store_order_events (
|
|
38
|
+
seq BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
39
|
+
order_id TEXT NOT NULL REFERENCES store_orders(id), from_state TEXT, to_state TEXT NOT NULL,
|
|
40
|
+
reason TEXT NOT NULL, actor TEXT NOT NULL, request_id TEXT NOT NULL,
|
|
41
|
+
occurred_at TEXT NOT NULL, receipt_refs TEXT NOT NULL)''')
|
|
42
|
+
conn.execute('CREATE INDEX IF NOT EXISTS store_order_events_order ON store_order_events(order_id,seq)')
|
|
43
|
+
conn.execute('''DO $$ BEGIN
|
|
44
|
+
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname='store_order_events_append_only'
|
|
45
|
+
AND tgrelid='store_order_events'::regclass) THEN
|
|
46
|
+
CREATE TRIGGER store_order_events_append_only BEFORE UPDATE OR DELETE ON store_order_events
|
|
47
|
+
FOR EACH ROW EXECUTE FUNCTION msg_money_ledger_append_only();
|
|
48
|
+
END IF;
|
|
49
|
+
END $$''')
|