@amityco/social-plus-vise 0.8.1 → 0.12.2

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.
@@ -8,10 +8,31 @@
8
8
  "title": "Android INTERNET permission must be declared",
9
9
  "severity": "error",
10
10
  "rationale": "The SDK needs network access. Missing INTERNET permission blocks social.plus API calls on Android.",
11
- "applies_when": { "platforms": ["android"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
11
+ "applies_when": {
12
+ "platforms": [
13
+ "android"
14
+ ],
15
+ "outcomes": [
16
+ "setup-sdk",
17
+ "setup-push",
18
+ "setup-live-data",
19
+ "add-feed",
20
+ "validate-setup"
21
+ ]
22
+ },
12
23
  "enforcement": {
13
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "android.permission.internet" }],
14
- "attestation": { "allowed": false, "host_agent_min_confidence": "high", "human_allowed": false, "evidence_required": [] }
24
+ "deterministic": [
25
+ {
26
+ "check": "validator-finding-absent",
27
+ "finding_rule_id": "android.permission.internet"
28
+ }
29
+ ],
30
+ "attestation": {
31
+ "allowed": false,
32
+ "host_agent_min_confidence": "high",
33
+ "human_allowed": false,
34
+ "evidence_required": []
35
+ }
15
36
  }
16
37
  },
17
38
  {
@@ -20,16 +41,40 @@
20
41
  "title": "Android SDK setup should use an Application class",
21
42
  "severity": "warning",
22
43
  "rationale": "Application startup avoids repeated SDK setup from Activity, Fragment, or UI lifecycle recreation.",
23
- "applies_when": { "platforms": ["android"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
44
+ "applies_when": {
45
+ "platforms": [
46
+ "android"
47
+ ],
48
+ "outcomes": [
49
+ "setup-sdk",
50
+ "setup-push",
51
+ "setup-live-data",
52
+ "add-feed",
53
+ "validate-setup"
54
+ ]
55
+ },
24
56
  "enforcement": {
25
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "android.application.class" }],
57
+ "deterministic": [
58
+ {
59
+ "check": "validator-finding-absent",
60
+ "finding_rule_id": "android.application.class"
61
+ }
62
+ ],
26
63
  "attestation": {
27
64
  "allowed": true,
28
65
  "host_agent_min_confidence": "high",
29
66
  "human_allowed": true,
30
67
  "evidence_required": [
31
- { "field": "file_path", "description": "Application or startup file containing SDK setup.", "upload_policy": "redact-paths-under: [\"src/secrets/\", \"config/credentials/\"]" },
32
- { "field": "entrypoint_chain", "description": "Call chain from app startup to SDK setup.", "upload_policy": "upload-with-consent" }
68
+ {
69
+ "field": "file_path",
70
+ "description": "Application or startup file containing SDK setup.",
71
+ "upload_policy": "redact-paths-under: [\"src/secrets/\", \"config/credentials/\"]"
72
+ },
73
+ {
74
+ "field": "entrypoint_chain",
75
+ "description": "Call chain from app startup to SDK setup.",
76
+ "upload_policy": "upload-with-consent"
77
+ }
33
78
  ]
34
79
  }
35
80
  }
@@ -40,10 +85,37 @@
40
85
  "title": "Android SDK setup must be present",
41
86
  "severity": "warning",
42
87
  "rationale": "The SDK must be initialized before login or social.plus API usage.",
43
- "applies_when": { "platforms": ["android"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
88
+ "applies_when": {
89
+ "platforms": [
90
+ "android"
91
+ ],
92
+ "outcomes": [
93
+ "setup-sdk",
94
+ "setup-push",
95
+ "setup-live-data",
96
+ "add-feed",
97
+ "validate-setup"
98
+ ]
99
+ },
44
100
  "enforcement": {
45
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "android.setup.present" }],
46
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "file_path", "description": "Source file containing SDK setup.", "upload_policy": "upload-with-consent" }] }
101
+ "deterministic": [
102
+ {
103
+ "check": "validator-finding-absent",
104
+ "finding_rule_id": "android.setup.present"
105
+ }
106
+ ],
107
+ "attestation": {
108
+ "allowed": true,
109
+ "host_agent_min_confidence": "high",
110
+ "human_allowed": true,
111
+ "evidence_required": [
112
+ {
113
+ "field": "file_path",
114
+ "description": "Source file containing SDK setup.",
115
+ "upload_policy": "upload-with-consent"
116
+ }
117
+ ]
118
+ }
47
119
  }
48
120
  },
49
121
  {
@@ -52,10 +124,37 @@
52
124
  "title": "Android SDK setup should not run in screen lifecycle",
53
125
  "severity": "warning",
54
126
  "rationale": "Screen lifecycle setup can reinitialize the SDK and create race conditions.",
55
- "applies_when": { "platforms": ["android"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
127
+ "applies_when": {
128
+ "platforms": [
129
+ "android"
130
+ ],
131
+ "outcomes": [
132
+ "setup-sdk",
133
+ "setup-push",
134
+ "setup-live-data",
135
+ "add-feed",
136
+ "validate-setup"
137
+ ]
138
+ },
56
139
  "enforcement": {
57
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "android.setup.lifecycle" }],
58
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "entrypoint_chain", "description": "Call chain proving setup runs once at app startup.", "upload_policy": "upload-with-consent" }] }
140
+ "deterministic": [
141
+ {
142
+ "check": "validator-finding-absent",
143
+ "finding_rule_id": "android.setup.lifecycle"
144
+ }
145
+ ],
146
+ "attestation": {
147
+ "allowed": true,
148
+ "host_agent_min_confidence": "high",
149
+ "human_allowed": true,
150
+ "evidence_required": [
151
+ {
152
+ "field": "entrypoint_chain",
153
+ "description": "Call chain proving setup runs once at app startup.",
154
+ "upload_policy": "upload-with-consent"
155
+ }
156
+ ]
157
+ }
59
158
  }
60
159
  },
61
160
  {
@@ -63,12 +162,44 @@
63
162
  "version": 1,
64
163
  "title": "Android setup must specify region or endpoint",
65
164
  "severity": "warning",
66
- "rationale": "The SDK region must match the customer's social.plus console project.",
67
- "applies_when": { "platforms": ["android"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
165
+ "rationale": "The SDK region must match the customer's social.plus console project. Reuse the app's existing region or endpoint source of truth when repairing setup, and avoid hardcoding a guessed default if the customer already has a config owner.",
166
+ "applies_when": {
167
+ "platforms": [
168
+ "android"
169
+ ],
170
+ "outcomes": [
171
+ "setup-sdk",
172
+ "setup-push",
173
+ "setup-live-data",
174
+ "add-feed",
175
+ "validate-setup"
176
+ ]
177
+ },
68
178
  "enforcement": {
69
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "android.setup.region" }],
70
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "region_source", "description": "Where region or endpoint is configured.", "upload_policy": "upload-with-consent" }] }
71
- }
179
+ "deterministic": [
180
+ {
181
+ "check": "validator-finding-absent",
182
+ "finding_rule_id": "android.setup.region"
183
+ }
184
+ ],
185
+ "attestation": {
186
+ "allowed": true,
187
+ "host_agent_min_confidence": "high",
188
+ "human_allowed": true,
189
+ "evidence_required": [
190
+ {
191
+ "field": "region_source",
192
+ "description": "Where region or endpoint is configured.",
193
+ "upload_policy": "upload-with-consent"
194
+ }
195
+ ]
196
+ }
197
+ },
198
+ "symptoms": [
199
+ "ENOTFOUND",
200
+ "NetworkError",
201
+ "invalid region"
202
+ ]
72
203
  },
73
204
  {
74
205
  "id": "android.login.present",
@@ -76,10 +207,37 @@
76
207
  "title": "Android login must be present",
77
208
  "severity": "warning",
78
209
  "rationale": "Login must happen after user identity is known and before subscriptions or API calls.",
79
- "applies_when": { "platforms": ["android"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
210
+ "applies_when": {
211
+ "platforms": [
212
+ "android"
213
+ ],
214
+ "outcomes": [
215
+ "setup-sdk",
216
+ "setup-push",
217
+ "setup-live-data",
218
+ "add-feed",
219
+ "validate-setup"
220
+ ]
221
+ },
80
222
  "enforcement": {
81
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "android.login.present" }],
82
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "login_flow", "description": "Where login is called after user identity is known.", "upload_policy": "upload-with-consent" }] }
223
+ "deterministic": [
224
+ {
225
+ "check": "validator-finding-absent",
226
+ "finding_rule_id": "android.login.present"
227
+ }
228
+ ],
229
+ "attestation": {
230
+ "allowed": true,
231
+ "host_agent_min_confidence": "high",
232
+ "human_allowed": true,
233
+ "evidence_required": [
234
+ {
235
+ "field": "login_flow",
236
+ "description": "Where login is called after user identity is known.",
237
+ "upload_policy": "upload-with-consent"
238
+ }
239
+ ]
240
+ }
83
241
  }
84
242
  },
85
243
  {
@@ -88,10 +246,37 @@
88
246
  "title": "Android SDK dependency should not use a floating version",
89
247
  "severity": "warning",
90
248
  "rationale": "Floating SDK versions can change APIs or behavior between CI runs without an explicit customer review.",
91
- "applies_when": { "platforms": ["android"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
249
+ "applies_when": {
250
+ "platforms": [
251
+ "android"
252
+ ],
253
+ "outcomes": [
254
+ "setup-sdk",
255
+ "setup-push",
256
+ "setup-live-data",
257
+ "add-feed",
258
+ "validate-setup"
259
+ ]
260
+ },
92
261
  "enforcement": {
93
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "android.sdk.version.pinned" }],
94
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "version_source", "description": "Dependency file or version catalog proving the SDK version is controlled.", "upload_policy": "upload-with-consent" }] }
262
+ "deterministic": [
263
+ {
264
+ "check": "validator-finding-absent",
265
+ "finding_rule_id": "android.sdk.version.pinned"
266
+ }
267
+ ],
268
+ "attestation": {
269
+ "allowed": true,
270
+ "host_agent_min_confidence": "high",
271
+ "human_allowed": true,
272
+ "evidence_required": [
273
+ {
274
+ "field": "version_source",
275
+ "description": "Dependency file or version catalog proving the SDK version is controlled.",
276
+ "upload_policy": "upload-with-consent"
277
+ }
278
+ ]
279
+ }
95
280
  }
96
281
  },
97
282
  {
@@ -100,10 +285,37 @@
100
285
  "title": "Flutter binding must be initialized before async SDK setup",
101
286
  "severity": "warning",
102
287
  "rationale": "Flutter async setup should call WidgetsFlutterBinding.ensureInitialized before runApp.",
103
- "applies_when": { "platforms": ["flutter"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
288
+ "applies_when": {
289
+ "platforms": [
290
+ "flutter"
291
+ ],
292
+ "outcomes": [
293
+ "setup-sdk",
294
+ "setup-push",
295
+ "setup-live-data",
296
+ "add-feed",
297
+ "validate-setup"
298
+ ]
299
+ },
104
300
  "enforcement": {
105
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "flutter.binding.initialized" }],
106
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "file_path", "description": "Flutter entrypoint file.", "upload_policy": "upload-with-consent" }] }
301
+ "deterministic": [
302
+ {
303
+ "check": "validator-finding-absent",
304
+ "finding_rule_id": "flutter.binding.initialized"
305
+ }
306
+ ],
307
+ "attestation": {
308
+ "allowed": true,
309
+ "host_agent_min_confidence": "high",
310
+ "human_allowed": true,
311
+ "evidence_required": [
312
+ {
313
+ "field": "file_path",
314
+ "description": "Flutter entrypoint file.",
315
+ "upload_policy": "upload-with-consent"
316
+ }
317
+ ]
318
+ }
107
319
  }
108
320
  },
109
321
  {
@@ -112,10 +324,37 @@
112
324
  "title": "Flutter SDK setup must be present",
113
325
  "severity": "warning",
114
326
  "rationale": "The SDK must be initialized before login or social.plus API usage.",
115
- "applies_when": { "platforms": ["flutter"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
327
+ "applies_when": {
328
+ "platforms": [
329
+ "flutter"
330
+ ],
331
+ "outcomes": [
332
+ "setup-sdk",
333
+ "setup-push",
334
+ "setup-live-data",
335
+ "add-feed",
336
+ "validate-setup"
337
+ ]
338
+ },
116
339
  "enforcement": {
117
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "flutter.setup.present" }],
118
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "file_path", "description": "Source file containing SDK setup.", "upload_policy": "upload-with-consent" }] }
340
+ "deterministic": [
341
+ {
342
+ "check": "validator-finding-absent",
343
+ "finding_rule_id": "flutter.setup.present"
344
+ }
345
+ ],
346
+ "attestation": {
347
+ "allowed": true,
348
+ "host_agent_min_confidence": "high",
349
+ "human_allowed": true,
350
+ "evidence_required": [
351
+ {
352
+ "field": "file_path",
353
+ "description": "Source file containing SDK setup.",
354
+ "upload_policy": "upload-with-consent"
355
+ }
356
+ ]
357
+ }
119
358
  }
120
359
  },
121
360
  {
@@ -123,12 +362,44 @@
123
362
  "version": 1,
124
363
  "title": "Flutter setup must specify region or endpoint",
125
364
  "severity": "warning",
126
- "rationale": "The SDK region must match the customer's social.plus console project.",
127
- "applies_when": { "platforms": ["flutter"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
365
+ "rationale": "The SDK region must match the customer's social.plus console project. Reuse the app's existing region or endpoint source of truth when one already exists, and do not hardcode a guessed default just to make setup appear green.",
366
+ "applies_when": {
367
+ "platforms": [
368
+ "flutter"
369
+ ],
370
+ "outcomes": [
371
+ "setup-sdk",
372
+ "setup-push",
373
+ "setup-live-data",
374
+ "add-feed",
375
+ "validate-setup"
376
+ ]
377
+ },
128
378
  "enforcement": {
129
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "flutter.setup.region" }],
130
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "region_source", "description": "Where region or endpoint is configured.", "upload_policy": "upload-with-consent" }] }
131
- }
379
+ "deterministic": [
380
+ {
381
+ "check": "validator-finding-absent",
382
+ "finding_rule_id": "flutter.setup.region"
383
+ }
384
+ ],
385
+ "attestation": {
386
+ "allowed": true,
387
+ "host_agent_min_confidence": "high",
388
+ "human_allowed": true,
389
+ "evidence_required": [
390
+ {
391
+ "field": "region_source",
392
+ "description": "Where region or endpoint is configured.",
393
+ "upload_policy": "upload-with-consent"
394
+ }
395
+ ]
396
+ }
397
+ },
398
+ "symptoms": [
399
+ "ENOTFOUND",
400
+ "NetworkError",
401
+ "invalid region"
402
+ ]
132
403
  },
133
404
  {
134
405
  "id": "flutter.login.present",
@@ -136,10 +407,37 @@
136
407
  "title": "Flutter login must be present",
137
408
  "severity": "warning",
138
409
  "rationale": "Login must happen after user identity is known and before subscriptions or API calls.",
139
- "applies_when": { "platforms": ["flutter"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
410
+ "applies_when": {
411
+ "platforms": [
412
+ "flutter"
413
+ ],
414
+ "outcomes": [
415
+ "setup-sdk",
416
+ "setup-push",
417
+ "setup-live-data",
418
+ "add-feed",
419
+ "validate-setup"
420
+ ]
421
+ },
140
422
  "enforcement": {
141
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "flutter.login.present" }],
142
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "login_flow", "description": "Where login is called after user identity is known.", "upload_policy": "upload-with-consent" }] }
423
+ "deterministic": [
424
+ {
425
+ "check": "validator-finding-absent",
426
+ "finding_rule_id": "flutter.login.present"
427
+ }
428
+ ],
429
+ "attestation": {
430
+ "allowed": true,
431
+ "host_agent_min_confidence": "high",
432
+ "human_allowed": true,
433
+ "evidence_required": [
434
+ {
435
+ "field": "login_flow",
436
+ "description": "Where login is called after user identity is known.",
437
+ "upload_policy": "upload-with-consent"
438
+ }
439
+ ]
440
+ }
143
441
  }
144
442
  },
145
443
  {
@@ -148,10 +446,37 @@
148
446
  "title": "Flutter SDK dependency should not use a floating version",
149
447
  "severity": "warning",
150
448
  "rationale": "Floating SDK versions can change APIs or behavior between CI runs without an explicit customer review.",
151
- "applies_when": { "platforms": ["flutter"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
449
+ "applies_when": {
450
+ "platforms": [
451
+ "flutter"
452
+ ],
453
+ "outcomes": [
454
+ "setup-sdk",
455
+ "setup-push",
456
+ "setup-live-data",
457
+ "add-feed",
458
+ "validate-setup"
459
+ ]
460
+ },
152
461
  "enforcement": {
153
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "flutter.sdk.version.pinned" }],
154
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "version_source", "description": "pubspec.yaml or dependency override proving the SDK version is controlled.", "upload_policy": "upload-with-consent" }] }
462
+ "deterministic": [
463
+ {
464
+ "check": "validator-finding-absent",
465
+ "finding_rule_id": "flutter.sdk.version.pinned"
466
+ }
467
+ ],
468
+ "attestation": {
469
+ "allowed": true,
470
+ "host_agent_min_confidence": "high",
471
+ "human_allowed": true,
472
+ "evidence_required": [
473
+ {
474
+ "field": "version_source",
475
+ "description": "pubspec.yaml or dependency override proving the SDK version is controlled.",
476
+ "upload_policy": "upload-with-consent"
477
+ }
478
+ ]
479
+ }
155
480
  }
156
481
  },
157
482
  {
@@ -160,10 +485,37 @@
160
485
  "title": "TypeScript SDK dependency should not use a floating version",
161
486
  "severity": "warning",
162
487
  "rationale": "Floating SDK versions can change APIs or behavior between CI runs without an explicit customer review.",
163
- "applies_when": { "platforms": ["typescript"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
488
+ "applies_when": {
489
+ "platforms": [
490
+ "typescript"
491
+ ],
492
+ "outcomes": [
493
+ "setup-sdk",
494
+ "setup-push",
495
+ "setup-live-data",
496
+ "add-feed",
497
+ "validate-setup"
498
+ ]
499
+ },
164
500
  "enforcement": {
165
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "typescript.sdk.version.pinned" }],
166
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "version_source", "description": "package.json, lockfile, or package manager catalog proving the SDK version is controlled.", "upload_policy": "upload-with-consent" }] }
501
+ "deterministic": [
502
+ {
503
+ "check": "validator-finding-absent",
504
+ "finding_rule_id": "typescript.sdk.version.pinned"
505
+ }
506
+ ],
507
+ "attestation": {
508
+ "allowed": true,
509
+ "host_agent_min_confidence": "high",
510
+ "human_allowed": true,
511
+ "evidence_required": [
512
+ {
513
+ "field": "version_source",
514
+ "description": "package.json, lockfile, or package manager catalog proving the SDK version is controlled.",
515
+ "upload_policy": "upload-with-consent"
516
+ }
517
+ ]
518
+ }
167
519
  }
168
520
  },
169
521
  {
@@ -172,10 +524,37 @@
172
524
  "title": "React Native SDK dependency should not use a floating version",
173
525
  "severity": "warning",
174
526
  "rationale": "Floating SDK versions can change APIs or behavior between CI runs without an explicit customer review.",
175
- "applies_when": { "platforms": ["react-native"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
527
+ "applies_when": {
528
+ "platforms": [
529
+ "react-native"
530
+ ],
531
+ "outcomes": [
532
+ "setup-sdk",
533
+ "setup-push",
534
+ "setup-live-data",
535
+ "add-feed",
536
+ "validate-setup"
537
+ ]
538
+ },
176
539
  "enforcement": {
177
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "react-native.sdk.version.pinned" }],
178
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "version_source", "description": "package.json, lockfile, or package manager catalog proving the SDK version is controlled.", "upload_policy": "upload-with-consent" }] }
540
+ "deterministic": [
541
+ {
542
+ "check": "validator-finding-absent",
543
+ "finding_rule_id": "react-native.sdk.version.pinned"
544
+ }
545
+ ],
546
+ "attestation": {
547
+ "allowed": true,
548
+ "host_agent_min_confidence": "high",
549
+ "human_allowed": true,
550
+ "evidence_required": [
551
+ {
552
+ "field": "version_source",
553
+ "description": "package.json, lockfile, or package manager catalog proving the SDK version is controlled.",
554
+ "upload_policy": "upload-with-consent"
555
+ }
556
+ ]
557
+ }
179
558
  }
180
559
  },
181
560
  {
@@ -184,10 +563,38 @@
184
563
  "title": "TypeScript client initialization must be present",
185
564
  "severity": "warning",
186
565
  "rationale": "The client must be created before login or social.plus API usage.",
187
- "applies_when": { "platforms": ["typescript", "react-native"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
566
+ "applies_when": {
567
+ "platforms": [
568
+ "typescript",
569
+ "react-native"
570
+ ],
571
+ "outcomes": [
572
+ "setup-sdk",
573
+ "setup-push",
574
+ "setup-live-data",
575
+ "add-feed",
576
+ "validate-setup"
577
+ ]
578
+ },
188
579
  "enforcement": {
189
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "typescript.client.create" }],
190
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "file_path", "description": "Source file containing client initialization.", "upload_policy": "upload-with-consent" }] }
580
+ "deterministic": [
581
+ {
582
+ "check": "validator-finding-absent",
583
+ "finding_rule_id": "typescript.client.create"
584
+ }
585
+ ],
586
+ "attestation": {
587
+ "allowed": true,
588
+ "host_agent_min_confidence": "high",
589
+ "human_allowed": true,
590
+ "evidence_required": [
591
+ {
592
+ "field": "file_path",
593
+ "description": "Source file containing client initialization.",
594
+ "upload_policy": "upload-with-consent"
595
+ }
596
+ ]
597
+ }
191
598
  }
192
599
  },
193
600
  {
@@ -195,12 +602,45 @@
195
602
  "version": 1,
196
603
  "title": "TypeScript client must specify region or endpoint",
197
604
  "severity": "warning",
198
- "rationale": "The SDK region must match the customer's social.plus console project.",
199
- "applies_when": { "platforms": ["typescript", "react-native"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
605
+ "rationale": "The SDK region must match the customer's social.plus console project. Reuse the app's existing region or endpoint source of truth when one already exists, and do not hardcode a guessed default just to make setup appear green.",
606
+ "applies_when": {
607
+ "platforms": [
608
+ "typescript",
609
+ "react-native"
610
+ ],
611
+ "outcomes": [
612
+ "setup-sdk",
613
+ "setup-push",
614
+ "setup-live-data",
615
+ "add-feed",
616
+ "validate-setup"
617
+ ]
618
+ },
200
619
  "enforcement": {
201
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "typescript.client.region" }],
202
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "region_source", "description": "Where region or endpoint is configured.", "upload_policy": "upload-with-consent" }] }
203
- }
620
+ "deterministic": [
621
+ {
622
+ "check": "validator-finding-absent",
623
+ "finding_rule_id": "typescript.client.region"
624
+ }
625
+ ],
626
+ "attestation": {
627
+ "allowed": true,
628
+ "host_agent_min_confidence": "high",
629
+ "human_allowed": true,
630
+ "evidence_required": [
631
+ {
632
+ "field": "region_source",
633
+ "description": "Where region or endpoint is configured.",
634
+ "upload_policy": "upload-with-consent"
635
+ }
636
+ ]
637
+ }
638
+ },
639
+ "symptoms": [
640
+ "ENOTFOUND",
641
+ "NetworkError",
642
+ "invalid region"
643
+ ]
204
644
  },
205
645
  {
206
646
  "id": "typescript.login.present",
@@ -208,10 +648,38 @@
208
648
  "title": "TypeScript login must be present",
209
649
  "severity": "warning",
210
650
  "rationale": "Login must happen after user identity is known and before subscriptions or API calls.",
211
- "applies_when": { "platforms": ["typescript", "react-native"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
651
+ "applies_when": {
652
+ "platforms": [
653
+ "typescript",
654
+ "react-native"
655
+ ],
656
+ "outcomes": [
657
+ "setup-sdk",
658
+ "setup-push",
659
+ "setup-live-data",
660
+ "add-feed",
661
+ "validate-setup"
662
+ ]
663
+ },
212
664
  "enforcement": {
213
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "typescript.login.present" }],
214
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "login_flow", "description": "Where login is called after user identity is known.", "upload_policy": "upload-with-consent" }] }
665
+ "deterministic": [
666
+ {
667
+ "check": "validator-finding-absent",
668
+ "finding_rule_id": "typescript.login.present"
669
+ }
670
+ ],
671
+ "attestation": {
672
+ "allowed": true,
673
+ "host_agent_min_confidence": "high",
674
+ "human_allowed": true,
675
+ "evidence_required": [
676
+ {
677
+ "field": "login_flow",
678
+ "description": "Where login is called after user identity is known.",
679
+ "upload_policy": "upload-with-consent"
680
+ }
681
+ ]
682
+ }
215
683
  }
216
684
  },
217
685
  {
@@ -220,10 +688,37 @@
220
688
  "title": "iOS project must have a dependency manifest",
221
689
  "severity": "warning",
222
690
  "rationale": "Foundry needs Podfile or Package.swift to verify the SDK dependency is present.",
223
- "applies_when": { "platforms": ["ios"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
691
+ "applies_when": {
692
+ "platforms": [
693
+ "ios"
694
+ ],
695
+ "outcomes": [
696
+ "setup-sdk",
697
+ "setup-push",
698
+ "setup-live-data",
699
+ "add-feed",
700
+ "validate-setup"
701
+ ]
702
+ },
224
703
  "enforcement": {
225
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "ios.dependency.manifest" }],
226
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "manifest_path", "description": "Path to the Podfile, Package.swift, or other iOS dependency manifest.", "upload_policy": "upload-with-consent" }] }
704
+ "deterministic": [
705
+ {
706
+ "check": "validator-finding-absent",
707
+ "finding_rule_id": "ios.dependency.manifest"
708
+ }
709
+ ],
710
+ "attestation": {
711
+ "allowed": true,
712
+ "host_agent_min_confidence": "high",
713
+ "human_allowed": true,
714
+ "evidence_required": [
715
+ {
716
+ "field": "manifest_path",
717
+ "description": "Path to the Podfile, Package.swift, or other iOS dependency manifest.",
718
+ "upload_policy": "upload-with-consent"
719
+ }
720
+ ]
721
+ }
227
722
  }
228
723
  },
229
724
  {
@@ -232,10 +727,37 @@
232
727
  "title": "iOS dependency manifest must include the social.plus SDK",
233
728
  "severity": "warning",
234
729
  "rationale": "The Amity iOS SDK dependency must be declared in the project's dependency manifest.",
235
- "applies_when": { "platforms": ["ios"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
730
+ "applies_when": {
731
+ "platforms": [
732
+ "ios"
733
+ ],
734
+ "outcomes": [
735
+ "setup-sdk",
736
+ "setup-push",
737
+ "setup-live-data",
738
+ "add-feed",
739
+ "validate-setup"
740
+ ]
741
+ },
236
742
  "enforcement": {
237
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "ios.dependency.sdk" }],
238
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "manifest_path", "description": "Manifest declaring the social.plus SDK dependency.", "upload_policy": "upload-with-consent" }] }
743
+ "deterministic": [
744
+ {
745
+ "check": "validator-finding-absent",
746
+ "finding_rule_id": "ios.dependency.sdk"
747
+ }
748
+ ],
749
+ "attestation": {
750
+ "allowed": true,
751
+ "host_agent_min_confidence": "high",
752
+ "human_allowed": true,
753
+ "evidence_required": [
754
+ {
755
+ "field": "manifest_path",
756
+ "description": "Manifest declaring the social.plus SDK dependency.",
757
+ "upload_policy": "upload-with-consent"
758
+ }
759
+ ]
760
+ }
239
761
  }
240
762
  },
241
763
  {
@@ -244,10 +766,37 @@
244
766
  "title": "iOS SDK dependency should not use a floating version",
245
767
  "severity": "warning",
246
768
  "rationale": "Floating SDK versions can change APIs or behavior between CI runs without an explicit customer review.",
247
- "applies_when": { "platforms": ["ios"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
769
+ "applies_when": {
770
+ "platforms": [
771
+ "ios"
772
+ ],
773
+ "outcomes": [
774
+ "setup-sdk",
775
+ "setup-push",
776
+ "setup-live-data",
777
+ "add-feed",
778
+ "validate-setup"
779
+ ]
780
+ },
248
781
  "enforcement": {
249
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "ios.sdk.version.pinned" }],
250
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "version_source", "description": "Package.swift, Podfile, or lockfile proving the SDK version is controlled.", "upload_policy": "upload-with-consent" }] }
782
+ "deterministic": [
783
+ {
784
+ "check": "validator-finding-absent",
785
+ "finding_rule_id": "ios.sdk.version.pinned"
786
+ }
787
+ ],
788
+ "attestation": {
789
+ "allowed": true,
790
+ "host_agent_min_confidence": "high",
791
+ "human_allowed": true,
792
+ "evidence_required": [
793
+ {
794
+ "field": "version_source",
795
+ "description": "Package.swift, Podfile, or lockfile proving the SDK version is controlled.",
796
+ "upload_policy": "upload-with-consent"
797
+ }
798
+ ]
799
+ }
251
800
  }
252
801
  },
253
802
  {
@@ -256,10 +805,37 @@
256
805
  "title": "iOS AmityClient must be initialized",
257
806
  "severity": "warning",
258
807
  "rationale": "The SDK must be initialized before login or social.plus API usage.",
259
- "applies_when": { "platforms": ["ios"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
808
+ "applies_when": {
809
+ "platforms": [
810
+ "ios"
811
+ ],
812
+ "outcomes": [
813
+ "setup-sdk",
814
+ "setup-push",
815
+ "setup-live-data",
816
+ "add-feed",
817
+ "validate-setup"
818
+ ]
819
+ },
260
820
  "enforcement": {
261
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "ios.setup.present" }],
262
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "file_path", "description": "Swift file containing AmityClient initialization.", "upload_policy": "upload-with-consent" }] }
821
+ "deterministic": [
822
+ {
823
+ "check": "validator-finding-absent",
824
+ "finding_rule_id": "ios.setup.present"
825
+ }
826
+ ],
827
+ "attestation": {
828
+ "allowed": true,
829
+ "host_agent_min_confidence": "high",
830
+ "human_allowed": true,
831
+ "evidence_required": [
832
+ {
833
+ "field": "file_path",
834
+ "description": "Swift file containing AmityClient initialization.",
835
+ "upload_policy": "upload-with-consent"
836
+ }
837
+ ]
838
+ }
263
839
  }
264
840
  },
265
841
  {
@@ -268,11 +844,43 @@
268
844
  "title": "iOS AmityClient initialization must specify region",
269
845
  "severity": "warning",
270
846
  "rationale": "The SDK region must match the customer's social.plus console project.",
271
- "applies_when": { "platforms": ["ios"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
847
+ "applies_when": {
848
+ "platforms": [
849
+ "ios"
850
+ ],
851
+ "outcomes": [
852
+ "setup-sdk",
853
+ "setup-push",
854
+ "setup-live-data",
855
+ "add-feed",
856
+ "validate-setup"
857
+ ]
858
+ },
272
859
  "enforcement": {
273
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "ios.setup.region" }],
274
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "region_source", "description": "Where region is configured (for example AmityRegion.EU).", "upload_policy": "upload-with-consent" }] }
275
- }
860
+ "deterministic": [
861
+ {
862
+ "check": "validator-finding-absent",
863
+ "finding_rule_id": "ios.setup.region"
864
+ }
865
+ ],
866
+ "attestation": {
867
+ "allowed": true,
868
+ "host_agent_min_confidence": "high",
869
+ "human_allowed": true,
870
+ "evidence_required": [
871
+ {
872
+ "field": "region_source",
873
+ "description": "Where region is configured (for example AmityRegion.EU).",
874
+ "upload_policy": "upload-with-consent"
875
+ }
876
+ ]
877
+ }
878
+ },
879
+ "symptoms": [
880
+ "ENOTFOUND",
881
+ "NetworkError",
882
+ "invalid region"
883
+ ]
276
884
  },
277
885
  {
278
886
  "id": "ios.login.present",
@@ -280,10 +888,37 @@
280
888
  "title": "iOS login must be present",
281
889
  "severity": "warning",
282
890
  "rationale": "Login must happen after user identity is known and before subscriptions or API calls.",
283
- "applies_when": { "platforms": ["ios"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
891
+ "applies_when": {
892
+ "platforms": [
893
+ "ios"
894
+ ],
895
+ "outcomes": [
896
+ "setup-sdk",
897
+ "setup-push",
898
+ "setup-live-data",
899
+ "add-feed",
900
+ "validate-setup"
901
+ ]
902
+ },
284
903
  "enforcement": {
285
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "ios.login.present" }],
286
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "login_flow", "description": "Where login is called after user identity is known.", "upload_policy": "upload-with-consent" }] }
904
+ "deterministic": [
905
+ {
906
+ "check": "validator-finding-absent",
907
+ "finding_rule_id": "ios.login.present"
908
+ }
909
+ ],
910
+ "attestation": {
911
+ "allowed": true,
912
+ "host_agent_min_confidence": "high",
913
+ "human_allowed": true,
914
+ "evidence_required": [
915
+ {
916
+ "field": "login_flow",
917
+ "description": "Where login is called after user identity is known.",
918
+ "upload_policy": "upload-with-consent"
919
+ }
920
+ ]
921
+ }
287
922
  }
288
923
  },
289
924
  {
@@ -292,59 +927,214 @@
292
927
  "title": "iOS SDK setup must not run inside a view or scene lifecycle",
293
928
  "severity": "warning",
294
929
  "rationale": "Initializing AmityClient inside viewDidLoad / viewWillAppear / scene(_:willConnectTo) re-runs setup as the user navigates between screens. Setup belongs in app process startup (AppDelegate.application(_:didFinishLaunchingWithOptions:) or @main App.init).",
295
- "applies_when": { "platforms": ["ios"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
930
+ "applies_when": {
931
+ "platforms": [
932
+ "ios"
933
+ ],
934
+ "outcomes": [
935
+ "setup-sdk",
936
+ "setup-push",
937
+ "setup-live-data",
938
+ "add-feed",
939
+ "validate-setup"
940
+ ]
941
+ },
296
942
  "enforcement": {
297
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "ios.setup.lifecycle" }],
298
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "entrypoint_chain", "description": "Call chain from app startup to AmityClient setup.", "upload_policy": "upload-with-consent" }] }
943
+ "deterministic": [
944
+ {
945
+ "check": "validator-finding-absent",
946
+ "finding_rule_id": "ios.setup.lifecycle"
947
+ }
948
+ ],
949
+ "attestation": {
950
+ "allowed": true,
951
+ "host_agent_min_confidence": "high",
952
+ "human_allowed": true,
953
+ "evidence_required": [
954
+ {
955
+ "field": "entrypoint_chain",
956
+ "description": "Call chain from app startup to AmityClient setup.",
957
+ "upload_policy": "upload-with-consent"
958
+ }
959
+ ]
960
+ }
299
961
  }
300
962
  },
301
963
  {
302
964
  "id": "android.session.renewal",
303
965
  "version": 1,
304
- "title": "Android login must pass an AmitySessionHandler that renews tokens",
966
+ "title": "Android login must pass a SessionHandler that renews tokens",
305
967
  "severity": "warning",
306
968
  "rationale": "Access tokens expire in production. Without a session handler that calls renewal.renew() in sessionWillRenewAccessToken, the SDK cannot refresh sessions and customers see silent auth failures.",
307
- "applies_when": { "platforms": ["android"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
969
+ "applies_when": {
970
+ "platforms": [
971
+ "android"
972
+ ],
973
+ "outcomes": [
974
+ "setup-sdk",
975
+ "setup-push",
976
+ "setup-live-data",
977
+ "add-feed",
978
+ "validate-setup"
979
+ ]
980
+ },
308
981
  "enforcement": {
309
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "android.session.renewal" }],
310
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "session_handler", "description": "Where AmitySessionHandler.sessionWillRenewAccessToken implementation lives.", "upload_policy": "upload-with-consent" }] }
311
- }
982
+ "deterministic": [
983
+ {
984
+ "check": "validator-finding-absent",
985
+ "finding_rule_id": "android.session.renewal"
986
+ }
987
+ ],
988
+ "attestation": {
989
+ "allowed": true,
990
+ "host_agent_min_confidence": "high",
991
+ "human_allowed": true,
992
+ "evidence_required": [
993
+ {
994
+ "field": "session_handler",
995
+ "description": "Where the SessionHandler.sessionWillRenewAccessToken implementation lives.",
996
+ "upload_policy": "upload-with-consent"
997
+ }
998
+ ]
999
+ }
1000
+ },
1001
+ "symptoms": [
1002
+ "401",
1003
+ "Unauthorized",
1004
+ "TokenExpiredException"
1005
+ ]
312
1006
  },
313
1007
  {
314
1008
  "id": "flutter.session.renewal",
315
1009
  "version": 1,
316
- "title": "Flutter login must pass an AmitySessionHandler that renews tokens",
1010
+ "title": "Flutter login must pass a session handler callback (AccessTokenRenewal renewal) that renews tokens",
317
1011
  "severity": "warning",
318
1012
  "rationale": "Access tokens expire in production. Without a session handler that calls renewal.renew() in sessionWillRenewAccessToken, the SDK cannot refresh sessions and customers see silent auth failures.",
319
- "applies_when": { "platforms": ["flutter"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
1013
+ "applies_when": {
1014
+ "platforms": [
1015
+ "flutter"
1016
+ ],
1017
+ "outcomes": [
1018
+ "setup-sdk",
1019
+ "setup-push",
1020
+ "setup-live-data",
1021
+ "add-feed",
1022
+ "validate-setup"
1023
+ ]
1024
+ },
320
1025
  "enforcement": {
321
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "flutter.session.renewal" }],
322
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "session_handler", "description": "Where the Dart session handler implementation lives.", "upload_policy": "upload-with-consent" }] }
323
- }
1026
+ "deterministic": [
1027
+ {
1028
+ "check": "validator-finding-absent",
1029
+ "finding_rule_id": "flutter.session.renewal"
1030
+ }
1031
+ ],
1032
+ "attestation": {
1033
+ "allowed": true,
1034
+ "host_agent_min_confidence": "high",
1035
+ "human_allowed": true,
1036
+ "evidence_required": [
1037
+ {
1038
+ "field": "session_handler",
1039
+ "description": "Where the Dart session handler implementation lives.",
1040
+ "upload_policy": "upload-with-consent"
1041
+ }
1042
+ ]
1043
+ }
1044
+ },
1045
+ "symptoms": [
1046
+ "401",
1047
+ "Unauthorized",
1048
+ "TokenExpiredException"
1049
+ ]
324
1050
  },
325
1051
  {
326
1052
  "id": "ios.session.renewal",
327
1053
  "version": 1,
328
- "title": "iOS login must pass an AmitySessionHandler that renews tokens",
1054
+ "title": "iOS login must pass a SessionHandler that renews tokens",
329
1055
  "severity": "warning",
330
- "rationale": "Access tokens expire in production. Without an AmitySessionHandler that calls renewal.renew() in sessionWillRenewAccessToken, the iOS SDK cannot refresh sessions and customers see silent auth failures.",
331
- "applies_when": { "platforms": ["ios"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
1056
+ "rationale": "Access tokens expire in production. Without a SessionHandler that calls renewal.renew() in sessionWillRenewAccessToken, the iOS SDK cannot refresh sessions and customers see silent auth failures.",
1057
+ "applies_when": {
1058
+ "platforms": [
1059
+ "ios"
1060
+ ],
1061
+ "outcomes": [
1062
+ "setup-sdk",
1063
+ "setup-push",
1064
+ "setup-live-data",
1065
+ "add-feed",
1066
+ "validate-setup"
1067
+ ]
1068
+ },
332
1069
  "enforcement": {
333
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "ios.session.renewal" }],
334
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "session_handler", "description": "Where AmitySessionHandler.sessionWillRenewAccessToken implementation lives.", "upload_policy": "upload-with-consent" }] }
335
- }
1070
+ "deterministic": [
1071
+ {
1072
+ "check": "validator-finding-absent",
1073
+ "finding_rule_id": "ios.session.renewal"
1074
+ }
1075
+ ],
1076
+ "attestation": {
1077
+ "allowed": true,
1078
+ "host_agent_min_confidence": "high",
1079
+ "human_allowed": true,
1080
+ "evidence_required": [
1081
+ {
1082
+ "field": "session_handler",
1083
+ "description": "Where the SessionHandler.sessionWillRenewAccessToken implementation lives.",
1084
+ "upload_policy": "upload-with-consent"
1085
+ }
1086
+ ]
1087
+ }
1088
+ },
1089
+ "symptoms": [
1090
+ "401",
1091
+ "Unauthorized",
1092
+ "TokenExpiredException"
1093
+ ]
336
1094
  },
337
1095
  {
338
1096
  "id": "typescript.session.renewal",
339
1097
  "version": 1,
340
1098
  "title": "TypeScript client login must pass a session handler that renews tokens",
341
1099
  "severity": "warning",
342
- "rationale": "Access tokens expire in production. Without a sessionHandler / sessionWillRenewAccessToken calling renewal.renew(), the SDK cannot refresh sessions and customers see silent auth failures.",
343
- "applies_when": { "platforms": ["typescript"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
1100
+ "rationale": "Access tokens expire in production. Without a sessionHandler / sessionWillRenewAccessToken calling renewal.renew(), the SDK cannot refresh sessions and customers see silent auth failures. Add the renewal handler in the existing login path and preserve the current session owner so the handler stays alive for the full session lifetime.",
1101
+ "applies_when": {
1102
+ "platforms": [
1103
+ "typescript"
1104
+ ],
1105
+ "outcomes": [
1106
+ "setup-sdk",
1107
+ "setup-push",
1108
+ "setup-live-data",
1109
+ "add-feed",
1110
+ "validate-setup"
1111
+ ]
1112
+ },
344
1113
  "enforcement": {
345
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "typescript.session.renewal" }],
346
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "session_handler", "description": "Where the TypeScript session handler that renews tokens lives.", "upload_policy": "upload-with-consent" }] }
347
- }
1114
+ "deterministic": [
1115
+ {
1116
+ "check": "validator-finding-absent",
1117
+ "finding_rule_id": "typescript.session.renewal"
1118
+ }
1119
+ ],
1120
+ "attestation": {
1121
+ "allowed": true,
1122
+ "host_agent_min_confidence": "high",
1123
+ "human_allowed": true,
1124
+ "evidence_required": [
1125
+ {
1126
+ "field": "session_handler",
1127
+ "description": "Where the TypeScript session handler that renews tokens lives.",
1128
+ "upload_policy": "upload-with-consent"
1129
+ }
1130
+ ]
1131
+ }
1132
+ },
1133
+ "symptoms": [
1134
+ "401",
1135
+ "Unauthorized",
1136
+ "TokenExpiredException"
1137
+ ]
348
1138
  },
349
1139
  {
350
1140
  "id": "react-native.session.renewal",
@@ -352,11 +1142,43 @@
352
1142
  "title": "React Native client login must pass a session handler that renews tokens",
353
1143
  "severity": "warning",
354
1144
  "rationale": "Access tokens expire in production. Without a sessionHandler / sessionWillRenewAccessToken calling renewal.renew(), the SDK cannot refresh sessions and customers see silent auth failures.",
355
- "applies_when": { "platforms": ["react-native"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
1145
+ "applies_when": {
1146
+ "platforms": [
1147
+ "react-native"
1148
+ ],
1149
+ "outcomes": [
1150
+ "setup-sdk",
1151
+ "setup-push",
1152
+ "setup-live-data",
1153
+ "add-feed",
1154
+ "validate-setup"
1155
+ ]
1156
+ },
356
1157
  "enforcement": {
357
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "react-native.session.renewal" }],
358
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "session_handler", "description": "Where the React Native session handler that renews tokens lives.", "upload_policy": "upload-with-consent" }] }
359
- }
1158
+ "deterministic": [
1159
+ {
1160
+ "check": "validator-finding-absent",
1161
+ "finding_rule_id": "react-native.session.renewal"
1162
+ }
1163
+ ],
1164
+ "attestation": {
1165
+ "allowed": true,
1166
+ "host_agent_min_confidence": "high",
1167
+ "human_allowed": true,
1168
+ "evidence_required": [
1169
+ {
1170
+ "field": "session_handler",
1171
+ "description": "Where the React Native session handler that renews tokens lives.",
1172
+ "upload_policy": "upload-with-consent"
1173
+ }
1174
+ ]
1175
+ }
1176
+ },
1177
+ "symptoms": [
1178
+ "401",
1179
+ "Unauthorized",
1180
+ "TokenExpiredException"
1181
+ ]
360
1182
  },
361
1183
  {
362
1184
  "id": "android.auth.no-literal-user-id",
@@ -364,10 +1186,37 @@
364
1186
  "title": "Android login must not pass a hardcoded user identity",
365
1187
  "severity": "warning",
366
1188
  "rationale": "userId comes from the host app's auth state (session, account manager, identity provider). A literal userId in source ships a single-user demo to production.",
367
- "applies_when": { "platforms": ["android"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
1189
+ "applies_when": {
1190
+ "platforms": [
1191
+ "android"
1192
+ ],
1193
+ "outcomes": [
1194
+ "setup-sdk",
1195
+ "setup-push",
1196
+ "setup-live-data",
1197
+ "add-feed",
1198
+ "validate-setup"
1199
+ ]
1200
+ },
368
1201
  "enforcement": {
369
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "android.auth.no-literal-user-id" }],
370
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "identity_source", "description": "Where the runtime userId comes from (DI scope, route param, account manager, etc.).", "upload_policy": "upload-with-consent" }] }
1202
+ "deterministic": [
1203
+ {
1204
+ "check": "validator-finding-absent",
1205
+ "finding_rule_id": "android.auth.no-literal-user-id"
1206
+ }
1207
+ ],
1208
+ "attestation": {
1209
+ "allowed": true,
1210
+ "host_agent_min_confidence": "high",
1211
+ "human_allowed": true,
1212
+ "evidence_required": [
1213
+ {
1214
+ "field": "identity_source",
1215
+ "description": "Where the runtime userId comes from (DI scope, route param, account manager, etc.).",
1216
+ "upload_policy": "upload-with-consent"
1217
+ }
1218
+ ]
1219
+ }
371
1220
  }
372
1221
  },
373
1222
  {
@@ -376,10 +1225,37 @@
376
1225
  "title": "Flutter login must not pass a hardcoded user identity",
377
1226
  "severity": "warning",
378
1227
  "rationale": "userId comes from the host app's auth state. A literal userId in source ships a single-user demo to production.",
379
- "applies_when": { "platforms": ["flutter"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
1228
+ "applies_when": {
1229
+ "platforms": [
1230
+ "flutter"
1231
+ ],
1232
+ "outcomes": [
1233
+ "setup-sdk",
1234
+ "setup-push",
1235
+ "setup-live-data",
1236
+ "add-feed",
1237
+ "validate-setup"
1238
+ ]
1239
+ },
380
1240
  "enforcement": {
381
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "flutter.auth.no-literal-user-id" }],
382
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "identity_source", "description": "Where the runtime userId comes from (provider, riverpod, bloc, etc.).", "upload_policy": "upload-with-consent" }] }
1241
+ "deterministic": [
1242
+ {
1243
+ "check": "validator-finding-absent",
1244
+ "finding_rule_id": "flutter.auth.no-literal-user-id"
1245
+ }
1246
+ ],
1247
+ "attestation": {
1248
+ "allowed": true,
1249
+ "host_agent_min_confidence": "high",
1250
+ "human_allowed": true,
1251
+ "evidence_required": [
1252
+ {
1253
+ "field": "identity_source",
1254
+ "description": "Where the runtime userId comes from (provider, riverpod, bloc, etc.).",
1255
+ "upload_policy": "upload-with-consent"
1256
+ }
1257
+ ]
1258
+ }
383
1259
  }
384
1260
  },
385
1261
  {
@@ -388,10 +1264,37 @@
388
1264
  "title": "iOS login must not pass a hardcoded user identity",
389
1265
  "severity": "warning",
390
1266
  "rationale": "userId comes from the host app's auth state (KeychainStore, identity provider, account manager). A literal userId in source ships a single-user demo to production.",
391
- "applies_when": { "platforms": ["ios"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
1267
+ "applies_when": {
1268
+ "platforms": [
1269
+ "ios"
1270
+ ],
1271
+ "outcomes": [
1272
+ "setup-sdk",
1273
+ "setup-push",
1274
+ "setup-live-data",
1275
+ "add-feed",
1276
+ "validate-setup"
1277
+ ]
1278
+ },
392
1279
  "enforcement": {
393
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "ios.auth.no-literal-user-id" }],
394
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "identity_source", "description": "Where the runtime userId comes from (Keychain, account service, dependency injection, etc.).", "upload_policy": "upload-with-consent" }] }
1280
+ "deterministic": [
1281
+ {
1282
+ "check": "validator-finding-absent",
1283
+ "finding_rule_id": "ios.auth.no-literal-user-id"
1284
+ }
1285
+ ],
1286
+ "attestation": {
1287
+ "allowed": true,
1288
+ "host_agent_min_confidence": "high",
1289
+ "human_allowed": true,
1290
+ "evidence_required": [
1291
+ {
1292
+ "field": "identity_source",
1293
+ "description": "Where the runtime userId comes from (Keychain, account service, dependency injection, etc.).",
1294
+ "upload_policy": "upload-with-consent"
1295
+ }
1296
+ ]
1297
+ }
395
1298
  }
396
1299
  },
397
1300
  {
@@ -400,10 +1303,37 @@
400
1303
  "title": "TypeScript login must not pass a hardcoded user identity",
401
1304
  "severity": "warning",
402
1305
  "rationale": "userId comes from the host app's auth state (auth context, route loader, session). A literal userId in source ships a single-user demo to production.",
403
- "applies_when": { "platforms": ["typescript"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
1306
+ "applies_when": {
1307
+ "platforms": [
1308
+ "typescript"
1309
+ ],
1310
+ "outcomes": [
1311
+ "setup-sdk",
1312
+ "setup-push",
1313
+ "setup-live-data",
1314
+ "add-feed",
1315
+ "validate-setup"
1316
+ ]
1317
+ },
404
1318
  "enforcement": {
405
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "typescript.auth.no-literal-user-id" }],
406
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "identity_source", "description": "Where the runtime userId comes from (auth context, session cookie, route loader, etc.).", "upload_policy": "upload-with-consent" }] }
1319
+ "deterministic": [
1320
+ {
1321
+ "check": "validator-finding-absent",
1322
+ "finding_rule_id": "typescript.auth.no-literal-user-id"
1323
+ }
1324
+ ],
1325
+ "attestation": {
1326
+ "allowed": true,
1327
+ "host_agent_min_confidence": "high",
1328
+ "human_allowed": true,
1329
+ "evidence_required": [
1330
+ {
1331
+ "field": "identity_source",
1332
+ "description": "Where the runtime userId comes from (auth context, session cookie, route loader, etc.).",
1333
+ "upload_policy": "upload-with-consent"
1334
+ }
1335
+ ]
1336
+ }
407
1337
  }
408
1338
  },
409
1339
  {
@@ -412,10 +1342,37 @@
412
1342
  "title": "React Native login must not pass a hardcoded user identity",
413
1343
  "severity": "warning",
414
1344
  "rationale": "userId comes from the host app's auth state. A literal userId in source ships a single-user demo to production.",
415
- "applies_when": { "platforms": ["react-native"], "outcomes": ["setup-sdk", "setup-push", "setup-live-data", "add-feed", "validate-setup"] },
1345
+ "applies_when": {
1346
+ "platforms": [
1347
+ "react-native"
1348
+ ],
1349
+ "outcomes": [
1350
+ "setup-sdk",
1351
+ "setup-push",
1352
+ "setup-live-data",
1353
+ "add-feed",
1354
+ "validate-setup"
1355
+ ]
1356
+ },
416
1357
  "enforcement": {
417
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "react-native.auth.no-literal-user-id" }],
418
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "identity_source", "description": "Where the runtime userId comes from (auth context, secure storage, etc.).", "upload_policy": "upload-with-consent" }] }
1358
+ "deterministic": [
1359
+ {
1360
+ "check": "validator-finding-absent",
1361
+ "finding_rule_id": "react-native.auth.no-literal-user-id"
1362
+ }
1363
+ ],
1364
+ "attestation": {
1365
+ "allowed": true,
1366
+ "host_agent_min_confidence": "high",
1367
+ "human_allowed": true,
1368
+ "evidence_required": [
1369
+ {
1370
+ "field": "identity_source",
1371
+ "description": "Where the runtime userId comes from (auth context, secure storage, etc.).",
1372
+ "upload_policy": "upload-with-consent"
1373
+ }
1374
+ ]
1375
+ }
419
1376
  }
420
1377
  },
421
1378
  {
@@ -423,11 +1380,35 @@
423
1380
  "version": 1,
424
1381
  "title": "Android session handler must be retained",
425
1382
  "severity": "warning",
426
- "rationale": "AmitySessionHandler declared as a function-local variable gets garbage collected. The SDK's renewal callback will then call into nothing, causing sessions to silently expire.",
427
- "applies_when": { "platforms": ["android"], "outcomes": ["setup-sdk", "validate-setup"] },
1383
+ "rationale": "A SessionHandler declared as a function-local variable gets garbage collected. The SDK's renewal callback will then call into nothing, causing sessions to silently expire.",
1384
+ "applies_when": {
1385
+ "platforms": [
1386
+ "android"
1387
+ ],
1388
+ "outcomes": [
1389
+ "setup-sdk",
1390
+ "validate-setup"
1391
+ ]
1392
+ },
428
1393
  "enforcement": {
429
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "android.session-handler.retained" }],
430
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "handler_retention", "description": "Where the handler is owned to prevent GC.", "upload_policy": "upload-with-consent" }] }
1394
+ "deterministic": [
1395
+ {
1396
+ "check": "validator-finding-absent",
1397
+ "finding_rule_id": "android.session-handler.retained"
1398
+ }
1399
+ ],
1400
+ "attestation": {
1401
+ "allowed": true,
1402
+ "host_agent_min_confidence": "high",
1403
+ "human_allowed": true,
1404
+ "evidence_required": [
1405
+ {
1406
+ "field": "handler_retention",
1407
+ "description": "Where the handler is owned to prevent GC.",
1408
+ "upload_policy": "upload-with-consent"
1409
+ }
1410
+ ]
1411
+ }
431
1412
  }
432
1413
  },
433
1414
  {
@@ -435,11 +1416,35 @@
435
1416
  "version": 1,
436
1417
  "title": "Flutter session handler must be retained",
437
1418
  "severity": "warning",
438
- "rationale": "AmitySessionHandler declared as a function-local variable gets garbage collected by Dart. The SDK's renewal callback will then call into nothing, causing sessions to silently expire.",
439
- "applies_when": { "platforms": ["flutter"], "outcomes": ["setup-sdk", "validate-setup"] },
1419
+ "rationale": "A session handler callback held only as a function-local variable gets garbage collected by Dart. The SDK's renewal callback will then call into nothing, causing sessions to silently expire.",
1420
+ "applies_when": {
1421
+ "platforms": [
1422
+ "flutter"
1423
+ ],
1424
+ "outcomes": [
1425
+ "setup-sdk",
1426
+ "validate-setup"
1427
+ ]
1428
+ },
440
1429
  "enforcement": {
441
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "flutter.session-handler.retained" }],
442
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "handler_retention", "description": "Where the handler is owned to prevent GC.", "upload_policy": "upload-with-consent" }] }
1430
+ "deterministic": [
1431
+ {
1432
+ "check": "validator-finding-absent",
1433
+ "finding_rule_id": "flutter.session-handler.retained"
1434
+ }
1435
+ ],
1436
+ "attestation": {
1437
+ "allowed": true,
1438
+ "host_agent_min_confidence": "high",
1439
+ "human_allowed": true,
1440
+ "evidence_required": [
1441
+ {
1442
+ "field": "handler_retention",
1443
+ "description": "Where the handler is owned to prevent GC.",
1444
+ "upload_policy": "upload-with-consent"
1445
+ }
1446
+ ]
1447
+ }
443
1448
  }
444
1449
  },
445
1450
  {
@@ -447,11 +1452,35 @@
447
1452
  "version": 1,
448
1453
  "title": "iOS session handler must be retained",
449
1454
  "severity": "warning",
450
- "rationale": "AmitySessionHandler declared as a function-local variable will be released by ARC. The SDK's renewal callback will then call into nothing, causing sessions to silently expire.",
451
- "applies_when": { "platforms": ["ios"], "outcomes": ["setup-sdk", "validate-setup"] },
1455
+ "rationale": "A SessionHandler declared as a function-local variable will be released by ARC. The SDK's renewal callback will then call into nothing, causing sessions to silently expire.",
1456
+ "applies_when": {
1457
+ "platforms": [
1458
+ "ios"
1459
+ ],
1460
+ "outcomes": [
1461
+ "setup-sdk",
1462
+ "validate-setup"
1463
+ ]
1464
+ },
452
1465
  "enforcement": {
453
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "ios.session-handler.retained" }],
454
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "handler_retention", "description": "Where the handler is owned to prevent GC.", "upload_policy": "upload-with-consent" }] }
1466
+ "deterministic": [
1467
+ {
1468
+ "check": "validator-finding-absent",
1469
+ "finding_rule_id": "ios.session-handler.retained"
1470
+ }
1471
+ ],
1472
+ "attestation": {
1473
+ "allowed": true,
1474
+ "host_agent_min_confidence": "high",
1475
+ "human_allowed": true,
1476
+ "evidence_required": [
1477
+ {
1478
+ "field": "handler_retention",
1479
+ "description": "Where the handler is owned to prevent GC.",
1480
+ "upload_policy": "upload-with-consent"
1481
+ }
1482
+ ]
1483
+ }
455
1484
  }
456
1485
  },
457
1486
  {
@@ -460,10 +1489,34 @@
460
1489
  "title": "TypeScript session handler must be retained",
461
1490
  "severity": "warning",
462
1491
  "rationale": "Session handler declared as a function-local variable gets garbage collected by JS. The SDK's renewal callback will then call into nothing, causing sessions to silently expire.",
463
- "applies_when": { "platforms": ["typescript"], "outcomes": ["setup-sdk", "validate-setup"] },
1492
+ "applies_when": {
1493
+ "platforms": [
1494
+ "typescript"
1495
+ ],
1496
+ "outcomes": [
1497
+ "setup-sdk",
1498
+ "validate-setup"
1499
+ ]
1500
+ },
464
1501
  "enforcement": {
465
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "typescript.session-handler.retained" }],
466
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "handler_retention", "description": "Where the handler is owned to prevent GC.", "upload_policy": "upload-with-consent" }] }
1502
+ "deterministic": [
1503
+ {
1504
+ "check": "validator-finding-absent",
1505
+ "finding_rule_id": "typescript.session-handler.retained"
1506
+ }
1507
+ ],
1508
+ "attestation": {
1509
+ "allowed": true,
1510
+ "host_agent_min_confidence": "high",
1511
+ "human_allowed": true,
1512
+ "evidence_required": [
1513
+ {
1514
+ "field": "handler_retention",
1515
+ "description": "Where the handler is owned to prevent GC.",
1516
+ "upload_policy": "upload-with-consent"
1517
+ }
1518
+ ]
1519
+ }
467
1520
  }
468
1521
  },
469
1522
  {
@@ -472,10 +1525,247 @@
472
1525
  "title": "React Native session handler must be retained",
473
1526
  "severity": "warning",
474
1527
  "rationale": "Session handler declared as a function-local variable gets garbage collected by JS. The SDK's renewal callback will then call into nothing, causing sessions to silently expire.",
475
- "applies_when": { "platforms": ["react-native"], "outcomes": ["setup-sdk", "validate-setup"] },
1528
+ "applies_when": {
1529
+ "platforms": [
1530
+ "react-native"
1531
+ ],
1532
+ "outcomes": [
1533
+ "setup-sdk",
1534
+ "validate-setup"
1535
+ ]
1536
+ },
1537
+ "enforcement": {
1538
+ "deterministic": [
1539
+ {
1540
+ "check": "validator-finding-absent",
1541
+ "finding_rule_id": "react-native.session-handler.retained"
1542
+ }
1543
+ ],
1544
+ "attestation": {
1545
+ "allowed": true,
1546
+ "host_agent_min_confidence": "high",
1547
+ "human_allowed": true,
1548
+ "evidence_required": [
1549
+ {
1550
+ "field": "handler_retention",
1551
+ "description": "Where the handler is owned to prevent GC.",
1552
+ "upload_policy": "upload-with-consent"
1553
+ }
1554
+ ]
1555
+ }
1556
+ }
1557
+ },
1558
+ {
1559
+ "id": "typescript.client.no-ssr-init",
1560
+ "version": 1,
1561
+ "title": "TypeScript SDK client must not be initialized in a server-side context",
1562
+ "severity": "error",
1563
+ "rationale": "TypeScript SDK client initialization (for example Client.create() or AmityClient.createClient()) requires browser APIs. Calling it in a Next.js Server Component, layout.tsx without 'use client', or inside getServerSideProps/getStaticProps causes a build crash or SSR 500 in production. next dev often recovers gracefully, making this invisible in development.",
1564
+ "applies_when": {
1565
+ "platforms": [
1566
+ "typescript"
1567
+ ],
1568
+ "outcomes": [
1569
+ "setup-sdk",
1570
+ "validate-setup"
1571
+ ]
1572
+ },
1573
+ "feedforward": "If this is a Next.js project, SDK initialization must happen client-side only. Wrap the client initialization call (Client.create() or createClient()) in a 'use client' component, a useEffect, or use dynamic() with { ssr: false }. Do not call it in a Server Component, layout file, or getServerSideProps.",
1574
+ "enforcement": {
1575
+ "deterministic": [
1576
+ {
1577
+ "check": "validator-finding-absent",
1578
+ "finding_rule_id": "typescript.client.no-ssr-init"
1579
+ }
1580
+ ],
1581
+ "attestation": {
1582
+ "allowed": true,
1583
+ "host_agent_min_confidence": "high",
1584
+ "human_allowed": true,
1585
+ "evidence_required": [
1586
+ {
1587
+ "field": "ssr_init_guard",
1588
+ "description": "Where the SDK is initialized inside a Next.js client-side context (e.g. 'use client', useEffect, etc.)",
1589
+ "upload_policy": "upload-with-consent"
1590
+ }
1591
+ ]
1592
+ }
1593
+ }
1594
+ },
1595
+ {
1596
+ "id": "react-native.posts.activity-tag-filter",
1597
+ "version": 1,
1598
+ "title": "Activity and toast hooks must scope getPosts with a tags filter",
1599
+ "severity": "warning",
1600
+ "rationale": "Without a tags filter, getPosts fetches every post in the community into the activity or toast pipeline. Only posts tagged with the expected activity type should enter the pipeline; otherwise unrelated posts produce spurious toasts.",
1601
+ "applies_when": {
1602
+ "platforms": [
1603
+ "react-native",
1604
+ "typescript"
1605
+ ],
1606
+ "outcomes": [
1607
+ "setup-live-data",
1608
+ "add-feed",
1609
+ "validate-setup"
1610
+ ]
1611
+ },
1612
+ "enforcement": {
1613
+ "deterministic": [
1614
+ {
1615
+ "check": "validator-finding-absent",
1616
+ "finding_rule_id": "react-native.posts.activity-tag-filter"
1617
+ }
1618
+ ],
1619
+ "attestation": {
1620
+ "allowed": true,
1621
+ "host_agent_min_confidence": "high",
1622
+ "human_allowed": true,
1623
+ "evidence_required": [
1624
+ {
1625
+ "field": "tag_strategy",
1626
+ "description": "Why all posts (not just tagged ones) should enter this pipeline, or the alternative scope mechanism used.",
1627
+ "upload_policy": "upload-with-consent"
1628
+ }
1629
+ ]
1630
+ }
1631
+ }
1632
+ },
1633
+ {
1634
+ "id": "react-native.posts.reaction-stale-post-ref",
1635
+ "version": 1,
1636
+ "title": "Reaction callbacks must use live post state, not a captured ref",
1637
+ "severity": "warning",
1638
+ "rationale": "A postId cached in a useRef at first subscription load goes stale when the pinned or active post changes (e.g. the song changes on a jukebox). Reactions then silently target the old post instead of the currently displayed one.",
1639
+ "applies_when": {
1640
+ "platforms": [
1641
+ "react-native",
1642
+ "typescript"
1643
+ ],
1644
+ "outcomes": [
1645
+ "setup-live-data",
1646
+ "add-feed",
1647
+ "validate-setup"
1648
+ ]
1649
+ },
1650
+ "enforcement": {
1651
+ "deterministic": [
1652
+ {
1653
+ "check": "validator-finding-absent",
1654
+ "finding_rule_id": "react-native.posts.reaction-stale-post-ref"
1655
+ }
1656
+ ],
1657
+ "attestation": {
1658
+ "allowed": true,
1659
+ "host_agent_min_confidence": "high",
1660
+ "human_allowed": true,
1661
+ "evidence_required": [
1662
+ {
1663
+ "field": "ref_lifecycle",
1664
+ "description": "Why the ref is always current (e.g. effect dependency array guarantees re-subscription before any user interaction).",
1665
+ "upload_policy": "upload-with-consent"
1666
+ }
1667
+ ]
1668
+ }
1669
+ }
1670
+ },
1671
+ {
1672
+ "id": "react-native.follow.status-subscription",
1673
+ "version": 1,
1674
+ "title": "Follow status must use a live subscription, not a one-shot query",
1675
+ "severity": "warning",
1676
+ "rationale": "getFollowStatus returns a live object. A one-shot call captures the state at mount time and never updates — follow/unfollow actions will not be reflected in the UI until the user navigates away and back. Subscribing keeps the button state current in real time.",
1677
+ "applies_when": {
1678
+ "platforms": ["react-native", "typescript"],
1679
+ "outcomes": ["setup-live-data", "add-feed", "validate-setup"]
1680
+ },
1681
+ "enforcement": {
1682
+ "deterministic": [{"check": "validator-finding-absent", "finding_rule_id": "react-native.follow.status-subscription"}],
1683
+ "attestation": {
1684
+ "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true,
1685
+ "evidence_required": [{"field": "follow_status_lifecycle", "description": "Why one-shot is sufficient (e.g. status is not expected to change during this session).", "upload_policy": "upload-with-consent"}]
1686
+ }
1687
+ }
1688
+ },
1689
+ {
1690
+ "id": "react-native.membership.use-live-collection",
1691
+ "version": 1,
1692
+ "title": "Membership queries should use the LiveCollection API, not a one-shot search",
1693
+ "severity": "warning",
1694
+ "rationale": "searchMembers is a promise-based one-shot fetch. It returns members at the moment of the call and never notifies the UI when the membership list changes. getMembers returns a LiveCollection whose dataUpdated callback fires on every change, keeping the list current in real time.",
1695
+ "applies_when": {
1696
+ "platforms": [
1697
+ "react-native",
1698
+ "typescript"
1699
+ ],
1700
+ "outcomes": [
1701
+ "setup-live-data",
1702
+ "add-feed",
1703
+ "validate-setup"
1704
+ ]
1705
+ },
1706
+ "enforcement": {
1707
+ "deterministic": [
1708
+ {
1709
+ "check": "validator-finding-absent",
1710
+ "finding_rule_id": "react-native.membership.use-live-collection"
1711
+ }
1712
+ ],
1713
+ "attestation": {
1714
+ "allowed": true,
1715
+ "host_agent_min_confidence": "high",
1716
+ "human_allowed": true,
1717
+ "evidence_required": [
1718
+ {
1719
+ "field": "one_shot_justification",
1720
+ "description": "Why a one-shot membership fetch is sufficient here (e.g. membership list is static for this use-case).",
1721
+ "upload_policy": "upload-with-consent"
1722
+ }
1723
+ ]
1724
+ }
1725
+ }
1726
+ },
1727
+ {
1728
+ "id": "typescript.chat.channel-type-dm",
1729
+ "version": 1,
1730
+ "title": "DM channels must use type 'conversation', not 'community'",
1731
+ "severity": "warning",
1732
+ "rationale": "createChannel with type 'community' creates a group channel visible to all community members. For a 1-to-1 private DM the type must be 'conversation'.",
1733
+ "applies_when": { "platforms": ["typescript"], "outcomes": ["add-chat", "validate-setup"] },
1734
+ "enforcement": {
1735
+ "deterministic": [{"check": "validator-finding-absent", "finding_rule_id": "typescript.chat.channel-type-dm"}],
1736
+ "attestation": {"allowed": true, "host_agent_min_confidence": "high", "human_allowed": true,
1737
+ "evidence_required": [{"field": "channel_type_justification", "upload_policy": "upload-with-consent"}]}
1738
+ }
1739
+ },
1740
+ {
1741
+ "id": "react-native.chat.channel-type-dm",
1742
+ "version": 1,
1743
+ "title": "DM channels must use type 'conversation', not 'community'",
1744
+ "severity": "warning",
1745
+ "rationale": "createChannel with type 'community' creates a group channel visible to all community members. For a 1-to-1 private DM the type must be 'conversation'. Agents without guidance consistently choose 'community' because it sounds plausible but silently creates the wrong channel shape.",
1746
+ "applies_when": {
1747
+ "platforms": ["react-native", "typescript"],
1748
+ "outcomes": ["add-chat", "validate-setup"]
1749
+ },
476
1750
  "enforcement": {
477
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "react-native.session-handler.retained" }],
478
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "handler_retention", "description": "Where the handler is owned to prevent GC.", "upload_policy": "upload-with-consent" }] }
1751
+ "deterministic": [
1752
+ {
1753
+ "check": "validator-finding-absent",
1754
+ "finding_rule_id": "react-native.chat.channel-type-dm"
1755
+ }
1756
+ ],
1757
+ "attestation": {
1758
+ "allowed": true,
1759
+ "host_agent_min_confidence": "high",
1760
+ "human_allowed": true,
1761
+ "evidence_required": [
1762
+ {
1763
+ "field": "channel_type_justification",
1764
+ "description": "Why 'community' channel type is correct for this use-case (e.g. broadcast channel, multi-member group).",
1765
+ "upload_policy": "upload-with-consent"
1766
+ }
1767
+ ]
1768
+ }
479
1769
  }
480
1770
  }
481
1771
  ]