spaps 0.8.2 ā 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI_TOOLS.json +2410 -346
- package/README.md +89 -2
- package/assets/local-runtime/docker-compose.yml +2 -2
- package/client.js +4 -0
- package/package.json +1 -1
- package/src/ai-tool-spec.js +282 -19
- package/src/auth/handlers.js +109 -0
- package/src/auth/surface.js +653 -0
- package/src/cli-dispatcher.js +345 -1
- package/src/doctor.js +97 -5
- package/src/domain-cli.js +1 -0
- package/src/handlers.js +390 -1
- package/src/help-system.js +1 -1
- package/src/local-server.js +41 -4
package/src/cli-dispatcher.js
CHANGED
|
@@ -60,6 +60,35 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
|
|
|
60
60
|
.option('--json', 'Output in JSON format');
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
function addAccessDecisionOptions(command) {
|
|
64
|
+
return command
|
|
65
|
+
.option('--actor-type <type>', 'Actor type', 'user')
|
|
66
|
+
.option('--actor-ref <ref>', 'Stable actor reference')
|
|
67
|
+
.option('--user-id <id>', 'User UUID')
|
|
68
|
+
.option('--email <email>', 'Actor email address')
|
|
69
|
+
.option('--agent-id <id>', 'Agent UUID')
|
|
70
|
+
.option('--authenticated <bool>', 'Actor authenticated state')
|
|
71
|
+
.requiredOption('--action <action>', 'Action to check')
|
|
72
|
+
.requiredOption('--resource-type <type>', 'Resource type')
|
|
73
|
+
.requiredOption('--resource-ref <ref>', 'Stable resource reference')
|
|
74
|
+
.option('--resource-id <id>', 'Resource UUID')
|
|
75
|
+
.option('--resource-key <key>', 'Resource key')
|
|
76
|
+
.option('--entitlement-key <key>', 'Required entitlement key')
|
|
77
|
+
.option('--entitlement-resource-type <type>', 'Entitlement resource type')
|
|
78
|
+
.option('--entitlement-resource-id <id>', 'Entitlement resource UUID')
|
|
79
|
+
.option('--policy-name <name>', 'Policy name to evaluate')
|
|
80
|
+
.option('--policy-context <json>', 'Policy context JSON')
|
|
81
|
+
.option('--usage-feature-key <key>', 'Usage-control feature key')
|
|
82
|
+
.option('--usage-dimensions <json>', 'Usage dimensions JSON')
|
|
83
|
+
.option('--x402-resource-key <key>', 'x402 resource key')
|
|
84
|
+
.option('--approval-id <id>', 'Approval UUID')
|
|
85
|
+
.option('--approval-required', 'Require an approval before action execution', false)
|
|
86
|
+
.option('--authority-scope <scope>', 'Authority scope', 'application')
|
|
87
|
+
.option('--context <json>', 'Decision context JSON')
|
|
88
|
+
.option('--idempotency-key <key>', 'Decision idempotency key')
|
|
89
|
+
.option('--correlation-id <id>', 'Correlation id');
|
|
90
|
+
}
|
|
91
|
+
|
|
63
92
|
// spaps home
|
|
64
93
|
const cmdHome = program
|
|
65
94
|
.command('home')
|
|
@@ -256,10 +285,18 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
|
|
|
256
285
|
.command('doctor')
|
|
257
286
|
.description('Diagnose local environment and config')
|
|
258
287
|
.option('-p, --port <port>', 'Port to check', String(DEFAULT_PORT))
|
|
288
|
+
.option('--server-url <url>', 'Full server URL (overrides --port and SPAPS_API_URL)')
|
|
289
|
+
.option('--origin <origin>', 'Browser origin to send with publishable-key auth diagnostics')
|
|
259
290
|
.option('-s, --stripe <mode>', 'Stripe mode: mock|real')
|
|
260
291
|
.option('--json', 'Output in JSON format')
|
|
261
292
|
.action(
|
|
262
|
-
makeAction('doctor', (opts, _cmd, isJson) => ({
|
|
293
|
+
makeAction('doctor', (opts, _cmd, isJson) => ({
|
|
294
|
+
port: Number(opts.port),
|
|
295
|
+
serverUrl: opts.serverUrl || null,
|
|
296
|
+
origin: opts.origin || null,
|
|
297
|
+
stripe: opts.stripe || null,
|
|
298
|
+
json: isJson,
|
|
299
|
+
}))
|
|
263
300
|
);
|
|
264
301
|
allowDryRun(cmdDoctor);
|
|
265
302
|
|
|
@@ -330,6 +367,77 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
|
|
|
330
367
|
);
|
|
331
368
|
allowDryRun(cmdToken);
|
|
332
369
|
|
|
370
|
+
// spaps auth <subcommand>
|
|
371
|
+
const cmdAuth = program
|
|
372
|
+
.command('auth')
|
|
373
|
+
.description('Auth discovery and local diagnostic commands')
|
|
374
|
+
.showHelpAfterError()
|
|
375
|
+
.showSuggestionAfterError();
|
|
376
|
+
if (!dryRun) {
|
|
377
|
+
cmdAuth.action(() => {
|
|
378
|
+
cmdAuth.outputHelp();
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
allowDryRun(cmdAuth);
|
|
382
|
+
|
|
383
|
+
function addAuthSubcommand(name, description, addOptions, shape) {
|
|
384
|
+
const command = cmdAuth.command(name).description(description);
|
|
385
|
+
addRemoteCommandOptions(command)
|
|
386
|
+
.option('--origin <origin>', 'Browser Origin header for publishable-key checks');
|
|
387
|
+
if (typeof addOptions === 'function') addOptions(command);
|
|
388
|
+
command.action(
|
|
389
|
+
makeAction('auth', (opts, _cmd, isJson) => ({
|
|
390
|
+
subcommand: name,
|
|
391
|
+
port: Number(opts.port) || DEFAULT_PORT,
|
|
392
|
+
serverUrl: opts.serverUrl || null,
|
|
393
|
+
origin: opts.origin || null,
|
|
394
|
+
...shape(opts),
|
|
395
|
+
json: isJson,
|
|
396
|
+
}))
|
|
397
|
+
);
|
|
398
|
+
allowDryRun(command);
|
|
399
|
+
return command;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
addAuthSubcommand(
|
|
403
|
+
'methods',
|
|
404
|
+
'Print the auth method matrix from GET /api/auth/methods',
|
|
405
|
+
null,
|
|
406
|
+
() => ({})
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
addAuthSubcommand(
|
|
410
|
+
'mfa-test',
|
|
411
|
+
'Exercise local TOTP MFA enrollment, activation, login challenge, verification, and cleanup',
|
|
412
|
+
(command) =>
|
|
413
|
+
command
|
|
414
|
+
.option('--email <email>', 'Local test user email (or SPAPS_TEST_EMAIL)')
|
|
415
|
+
.option('--password <password>', 'Local test user password (or SPAPS_TEST_PASSWORD)')
|
|
416
|
+
.option('--allow-remote', 'Allow running against a non-local server URL', false),
|
|
417
|
+
(opts) => ({
|
|
418
|
+
email: opts.email || null,
|
|
419
|
+
password: opts.password || null,
|
|
420
|
+
allowRemote: Boolean(opts.allowRemote),
|
|
421
|
+
})
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
addAuthSubcommand(
|
|
425
|
+
'sms-test',
|
|
426
|
+
'Request or verify an SMS OTP against local console SMS',
|
|
427
|
+
(command) =>
|
|
428
|
+
command
|
|
429
|
+
.option('--phone-number <phone>', 'E.164 phone number (or SPAPS_TEST_PHONE_NUMBER)')
|
|
430
|
+
.option('--challenge-id <id>', 'Existing SMS challenge id to verify')
|
|
431
|
+
.option('--code <code>', 'SMS code from local server logs')
|
|
432
|
+
.option('--allow-remote', 'Allow running against a non-local server URL', false),
|
|
433
|
+
(opts) => ({
|
|
434
|
+
phoneNumber: opts.phoneNumber || null,
|
|
435
|
+
challengeId: opts.challengeId || null,
|
|
436
|
+
code: opts.code || null,
|
|
437
|
+
allowRemote: Boolean(opts.allowRemote),
|
|
438
|
+
})
|
|
439
|
+
);
|
|
440
|
+
|
|
333
441
|
// spaps dayrate <subcommand>
|
|
334
442
|
const cmdDayrate = program
|
|
335
443
|
.command('dayrate <subcommand>')
|
|
@@ -646,6 +754,242 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
|
|
|
646
754
|
);
|
|
647
755
|
allowDryRun(cmdIssueReports);
|
|
648
756
|
|
|
757
|
+
// spaps access check
|
|
758
|
+
const cmdAccess = program
|
|
759
|
+
.command('access')
|
|
760
|
+
.description('Access decision commands')
|
|
761
|
+
.showHelpAfterError()
|
|
762
|
+
.showSuggestionAfterError();
|
|
763
|
+
if (!dryRun) {
|
|
764
|
+
cmdAccess.action(() => {
|
|
765
|
+
cmdAccess.outputHelp();
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
allowDryRun(cmdAccess);
|
|
769
|
+
|
|
770
|
+
const cmdAccessCheck = addAccessDecisionOptions(cmdAccess.command('check').description('Check whether an actor can perform an action'));
|
|
771
|
+
addRemoteCommandOptions(cmdAccessCheck).action(
|
|
772
|
+
makeAction('access', (opts, _cmd, isJson) => ({
|
|
773
|
+
subcommand: 'check',
|
|
774
|
+
port: Number(opts.port) || DEFAULT_PORT,
|
|
775
|
+
serverUrl: opts.serverUrl || null,
|
|
776
|
+
actorType: opts.actorType || 'user',
|
|
777
|
+
actorRef: opts.actorRef || null,
|
|
778
|
+
userId: opts.userId || null,
|
|
779
|
+
email: opts.email || null,
|
|
780
|
+
agentId: opts.agentId || null,
|
|
781
|
+
authenticated: opts.authenticated === undefined ? null : opts.authenticated,
|
|
782
|
+
action: opts.action || null,
|
|
783
|
+
resourceType: opts.resourceType || null,
|
|
784
|
+
resourceRef: opts.resourceRef || null,
|
|
785
|
+
resourceId: opts.resourceId || null,
|
|
786
|
+
resourceKey: opts.resourceKey || null,
|
|
787
|
+
entitlementKey: opts.entitlementKey || null,
|
|
788
|
+
entitlementResourceType: opts.entitlementResourceType || null,
|
|
789
|
+
entitlementResourceId: opts.entitlementResourceId || null,
|
|
790
|
+
policyName: opts.policyName || null,
|
|
791
|
+
policyContext: opts.policyContext || null,
|
|
792
|
+
usageFeatureKey: opts.usageFeatureKey || null,
|
|
793
|
+
usageDimensions: opts.usageDimensions || null,
|
|
794
|
+
x402ResourceKey: opts.x402ResourceKey || null,
|
|
795
|
+
approvalId: opts.approvalId || null,
|
|
796
|
+
approvalRequired: Boolean(opts.approvalRequired),
|
|
797
|
+
authorityScope: opts.authorityScope || 'application',
|
|
798
|
+
context: opts.context || null,
|
|
799
|
+
idempotencyKey: opts.idempotencyKey || null,
|
|
800
|
+
correlationId: opts.correlationId || null,
|
|
801
|
+
json: isJson,
|
|
802
|
+
}))
|
|
803
|
+
);
|
|
804
|
+
allowDryRun(cmdAccessCheck);
|
|
805
|
+
|
|
806
|
+
// spaps journey run
|
|
807
|
+
const cmdJourney = program
|
|
808
|
+
.command('journey')
|
|
809
|
+
.description('Prepare agent-safe next actions for an application journey')
|
|
810
|
+
.showHelpAfterError()
|
|
811
|
+
.showSuggestionAfterError();
|
|
812
|
+
if (!dryRun) {
|
|
813
|
+
cmdJourney.action(() => {
|
|
814
|
+
cmdJourney.outputHelp();
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
allowDryRun(cmdJourney);
|
|
818
|
+
|
|
819
|
+
const cmdJourneyRun = addAccessDecisionOptions(cmdJourney.command('run').description('Run the access-to-next-action preparation flow'));
|
|
820
|
+
addRemoteCommandOptions(
|
|
821
|
+
cmdJourneyRun
|
|
822
|
+
.option('--include-command-templates', 'Include command templates for operator-gated actions', false)
|
|
823
|
+
.option('--operator-gated', 'Request server-authorized operator-gated templates', false)
|
|
824
|
+
.option('--operator-labels <csv>', 'Comma-separated operator labels')
|
|
825
|
+
.option('--environment <environment>', 'Execution environment', 'production')
|
|
826
|
+
).action(
|
|
827
|
+
makeAction('journey', (opts, _cmd, isJson) => ({
|
|
828
|
+
subcommand: 'run',
|
|
829
|
+
port: Number(opts.port) || DEFAULT_PORT,
|
|
830
|
+
serverUrl: opts.serverUrl || null,
|
|
831
|
+
actorType: opts.actorType || 'user',
|
|
832
|
+
actorRef: opts.actorRef || null,
|
|
833
|
+
userId: opts.userId || null,
|
|
834
|
+
email: opts.email || null,
|
|
835
|
+
agentId: opts.agentId || null,
|
|
836
|
+
authenticated: opts.authenticated === undefined ? null : opts.authenticated,
|
|
837
|
+
action: opts.action || null,
|
|
838
|
+
resourceType: opts.resourceType || null,
|
|
839
|
+
resourceRef: opts.resourceRef || null,
|
|
840
|
+
resourceId: opts.resourceId || null,
|
|
841
|
+
resourceKey: opts.resourceKey || null,
|
|
842
|
+
entitlementKey: opts.entitlementKey || null,
|
|
843
|
+
entitlementResourceType: opts.entitlementResourceType || null,
|
|
844
|
+
entitlementResourceId: opts.entitlementResourceId || null,
|
|
845
|
+
policyName: opts.policyName || null,
|
|
846
|
+
policyContext: opts.policyContext || null,
|
|
847
|
+
usageFeatureKey: opts.usageFeatureKey || null,
|
|
848
|
+
usageDimensions: opts.usageDimensions || null,
|
|
849
|
+
x402ResourceKey: opts.x402ResourceKey || null,
|
|
850
|
+
approvalId: opts.approvalId || null,
|
|
851
|
+
approvalRequired: Boolean(opts.approvalRequired),
|
|
852
|
+
authorityScope: opts.authorityScope || 'application',
|
|
853
|
+
context: opts.context || null,
|
|
854
|
+
idempotencyKey: opts.idempotencyKey || null,
|
|
855
|
+
correlationId: opts.correlationId || null,
|
|
856
|
+
includeCommandTemplates: Boolean(opts.includeCommandTemplates),
|
|
857
|
+
operatorGated: Boolean(opts.operatorGated),
|
|
858
|
+
operatorLabels: opts.operatorLabels || null,
|
|
859
|
+
environment: opts.environment || 'production',
|
|
860
|
+
json: isJson,
|
|
861
|
+
}))
|
|
862
|
+
);
|
|
863
|
+
allowDryRun(cmdJourneyRun);
|
|
864
|
+
|
|
865
|
+
// spaps graph <nodes|paths|impact>
|
|
866
|
+
const cmdGraph = program
|
|
867
|
+
.command('graph')
|
|
868
|
+
.description('Inspect the materialized SPAPS capability graph')
|
|
869
|
+
.showHelpAfterError()
|
|
870
|
+
.showSuggestionAfterError();
|
|
871
|
+
if (!dryRun) {
|
|
872
|
+
cmdGraph.action(() => {
|
|
873
|
+
cmdGraph.outputHelp();
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
allowDryRun(cmdGraph);
|
|
877
|
+
|
|
878
|
+
addRemoteCommandOptions(
|
|
879
|
+
cmdGraph.command('nodes')
|
|
880
|
+
.description('List capability graph nodes')
|
|
881
|
+
.option('--application-id <id>', 'Explicit application id for super-admin reads')
|
|
882
|
+
.option('--node-type <type>', 'Filter by node type')
|
|
883
|
+
.option('--status <status>', 'Filter by row status', 'active')
|
|
884
|
+
.option('-q, --query <query>', 'Case-insensitive label search')
|
|
885
|
+
.option('--cursor <cursor>', 'Pagination cursor')
|
|
886
|
+
.option('--limit <n>', 'Pagination limit')
|
|
887
|
+
).action(
|
|
888
|
+
makeAction('graph', (opts, _cmd, isJson) => ({
|
|
889
|
+
subcommand: 'nodes',
|
|
890
|
+
port: Number(opts.port) || DEFAULT_PORT,
|
|
891
|
+
serverUrl: opts.serverUrl || null,
|
|
892
|
+
applicationId: opts.applicationId || null,
|
|
893
|
+
nodeType: opts.nodeType || null,
|
|
894
|
+
status: opts.status || 'active',
|
|
895
|
+
query: opts.query || null,
|
|
896
|
+
cursor: opts.cursor || null,
|
|
897
|
+
limit: opts.limit ? Number(opts.limit) : null,
|
|
898
|
+
json: isJson,
|
|
899
|
+
}))
|
|
900
|
+
);
|
|
901
|
+
|
|
902
|
+
addRemoteCommandOptions(
|
|
903
|
+
cmdGraph.command('paths')
|
|
904
|
+
.description('Find bounded paths between two graph nodes')
|
|
905
|
+
.requiredOption('--from <node_key>', 'Starting node key')
|
|
906
|
+
.requiredOption('--to <node_key>', 'Target node key')
|
|
907
|
+
.option('--application-id <id>', 'Explicit application id for super-admin reads')
|
|
908
|
+
.option('--max-depth <n>', 'Maximum path depth')
|
|
909
|
+
.option('--limit <n>', 'Path limit')
|
|
910
|
+
.option('--include-stale', 'Include stale graph rows', false)
|
|
911
|
+
).action(
|
|
912
|
+
makeAction('graph', (opts, _cmd, isJson) => ({
|
|
913
|
+
subcommand: 'paths',
|
|
914
|
+
port: Number(opts.port) || DEFAULT_PORT,
|
|
915
|
+
serverUrl: opts.serverUrl || null,
|
|
916
|
+
applicationId: opts.applicationId || null,
|
|
917
|
+
fromNodeKey: opts.from || null,
|
|
918
|
+
toNodeKey: opts.to || null,
|
|
919
|
+
maxDepth: opts.maxDepth ? Number(opts.maxDepth) : null,
|
|
920
|
+
limit: opts.limit ? Number(opts.limit) : null,
|
|
921
|
+
includeStale: Boolean(opts.includeStale),
|
|
922
|
+
json: isJson,
|
|
923
|
+
}))
|
|
924
|
+
);
|
|
925
|
+
|
|
926
|
+
addRemoteCommandOptions(
|
|
927
|
+
cmdGraph.command('impact')
|
|
928
|
+
.description('Traverse outward from one graph node')
|
|
929
|
+
.requiredOption('--node-key <node_key>', 'Starting node key')
|
|
930
|
+
.option('--application-id <id>', 'Explicit application id for super-admin reads')
|
|
931
|
+
.option('--max-depth <n>', 'Maximum traversal depth')
|
|
932
|
+
.option('--limit <n>', 'Result limit')
|
|
933
|
+
.option('--include-stale', 'Include stale graph rows', false)
|
|
934
|
+
).action(
|
|
935
|
+
makeAction('graph', (opts, _cmd, isJson) => ({
|
|
936
|
+
subcommand: 'impact',
|
|
937
|
+
port: Number(opts.port) || DEFAULT_PORT,
|
|
938
|
+
serverUrl: opts.serverUrl || null,
|
|
939
|
+
applicationId: opts.applicationId || null,
|
|
940
|
+
nodeKey: opts.nodeKey || null,
|
|
941
|
+
maxDepth: opts.maxDepth ? Number(opts.maxDepth) : null,
|
|
942
|
+
limit: opts.limit ? Number(opts.limit) : null,
|
|
943
|
+
includeStale: Boolean(opts.includeStale),
|
|
944
|
+
json: isJson,
|
|
945
|
+
}))
|
|
946
|
+
);
|
|
947
|
+
|
|
948
|
+
addRemoteCommandOptions(
|
|
949
|
+
cmdGraph.command('refresh')
|
|
950
|
+
.description('Refresh the capability graph projection')
|
|
951
|
+
.option('--application-id <id>', 'Explicit application id for super-admin refresh')
|
|
952
|
+
.option('--correlation-id <id>', 'Correlation id for audit and projection diagnostics')
|
|
953
|
+
).action(
|
|
954
|
+
makeAction('graph', (opts, _cmd, isJson) => ({
|
|
955
|
+
subcommand: 'refresh',
|
|
956
|
+
port: Number(opts.port) || DEFAULT_PORT,
|
|
957
|
+
serverUrl: opts.serverUrl || null,
|
|
958
|
+
applicationId: opts.applicationId || null,
|
|
959
|
+
correlationId: opts.correlationId || null,
|
|
960
|
+
json: isJson,
|
|
961
|
+
}))
|
|
962
|
+
);
|
|
963
|
+
|
|
964
|
+
// spaps explain <decision-id>
|
|
965
|
+
const cmdExplain = addRemoteCommandOptions(
|
|
966
|
+
program
|
|
967
|
+
.command('explain <decision-id>')
|
|
968
|
+
.description('Explain one persisted access decision trace')
|
|
969
|
+
).action(
|
|
970
|
+
makeAction('explain', (opts, cmd, isJson) => ({
|
|
971
|
+
decisionId: cmd.args[0],
|
|
972
|
+
port: Number(opts.port) || DEFAULT_PORT,
|
|
973
|
+
serverUrl: opts.serverUrl || null,
|
|
974
|
+
json: isJson,
|
|
975
|
+
}))
|
|
976
|
+
);
|
|
977
|
+
allowDryRun(cmdExplain);
|
|
978
|
+
|
|
979
|
+
// spaps contract
|
|
980
|
+
const cmdContract = addRemoteCommandOptions(
|
|
981
|
+
program
|
|
982
|
+
.command('contract')
|
|
983
|
+
.description('Fetch the capability graph client contract')
|
|
984
|
+
).action(
|
|
985
|
+
makeAction('contract', (opts, _cmd, isJson) => ({
|
|
986
|
+
port: Number(opts.port) || DEFAULT_PORT,
|
|
987
|
+
serverUrl: opts.serverUrl || null,
|
|
988
|
+
json: isJson,
|
|
989
|
+
}))
|
|
990
|
+
);
|
|
991
|
+
allowDryRun(cmdContract);
|
|
992
|
+
|
|
649
993
|
return { program, getIntents: () => intents };
|
|
650
994
|
}
|
|
651
995
|
|
package/src/doctor.js
CHANGED
|
@@ -7,6 +7,11 @@ const chalk = require('chalk');
|
|
|
7
7
|
const { getServerStatus } = require('./ai-helper');
|
|
8
8
|
const { DEFAULT_PORT } = require('./config');
|
|
9
9
|
const { listDomains } = require('./domains');
|
|
10
|
+
const {
|
|
11
|
+
buildAuthDoctorChecks,
|
|
12
|
+
fetchAuthMethods,
|
|
13
|
+
resolveDiagnosticOrigin,
|
|
14
|
+
} = require('./auth/surface');
|
|
10
15
|
|
|
11
16
|
function checkNodeVersion() {
|
|
12
17
|
const version = process.versions.node || '0.0.0';
|
|
@@ -20,9 +25,9 @@ function checkNodeVersion() {
|
|
|
20
25
|
};
|
|
21
26
|
}
|
|
22
27
|
|
|
23
|
-
async function checkPort(port) {
|
|
28
|
+
async function checkPort(port, serverUrl = null) {
|
|
24
29
|
// If server is running, we consider port check OK
|
|
25
|
-
const status = await getServerStatus(port);
|
|
30
|
+
const status = await getServerStatus({ port, serverUrl });
|
|
26
31
|
if (status.running) {
|
|
27
32
|
return {
|
|
28
33
|
check: 'port',
|
|
@@ -31,6 +36,14 @@ async function checkPort(port) {
|
|
|
31
36
|
fix: null
|
|
32
37
|
};
|
|
33
38
|
}
|
|
39
|
+
if (serverUrl) {
|
|
40
|
+
return {
|
|
41
|
+
check: 'port',
|
|
42
|
+
success: false,
|
|
43
|
+
details: { port, running: false, url: status.url, error: status.error || null },
|
|
44
|
+
fix: `Check SPAPS_API_URL or --server-url (${status.url})`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
34
47
|
// Otherwise ensure port is free to bind
|
|
35
48
|
const free = await new Promise((resolve) => {
|
|
36
49
|
const tester = net.createServer()
|
|
@@ -224,22 +237,99 @@ async function checkDomainMounts(port) {
|
|
|
224
237
|
return Promise.all(listDomains().map((d) => probeDomain(port, d)));
|
|
225
238
|
}
|
|
226
239
|
|
|
240
|
+
async function checkAuthSurface({
|
|
241
|
+
port = DEFAULT_PORT,
|
|
242
|
+
serverUrl = null,
|
|
243
|
+
origin = null,
|
|
244
|
+
cwd = process.cwd(),
|
|
245
|
+
env = process.env,
|
|
246
|
+
} = {}) {
|
|
247
|
+
const runtime = await getServerStatus({ port, serverUrl });
|
|
248
|
+
if (!runtime.running) {
|
|
249
|
+
return [
|
|
250
|
+
{
|
|
251
|
+
check: 'auth_methods',
|
|
252
|
+
success: false,
|
|
253
|
+
details: { running: false, url: runtime.url, error: runtime.error || null },
|
|
254
|
+
fix: `Start the SPAPS server or pass --server-url to a reachable server before checking /api/auth/methods.`,
|
|
255
|
+
},
|
|
256
|
+
];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const resolvedOrigin = resolveDiagnosticOrigin({ origin });
|
|
260
|
+
try {
|
|
261
|
+
const discovery = await fetchAuthMethods({
|
|
262
|
+
port,
|
|
263
|
+
serverUrl: runtime.url,
|
|
264
|
+
origin: resolvedOrigin,
|
|
265
|
+
cwd,
|
|
266
|
+
env,
|
|
267
|
+
});
|
|
268
|
+
const checks = await buildAuthDoctorChecks({
|
|
269
|
+
methods: discovery.methods,
|
|
270
|
+
runtime,
|
|
271
|
+
origin: resolvedOrigin,
|
|
272
|
+
});
|
|
273
|
+
const matrix = checks.find((check) => check.check === 'auth_methods');
|
|
274
|
+
if (matrix) {
|
|
275
|
+
matrix.details = {
|
|
276
|
+
...matrix.details,
|
|
277
|
+
server_url: discovery.serverUrl,
|
|
278
|
+
origin: resolvedOrigin,
|
|
279
|
+
api_key_source: discovery.apiKeySource,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return checks;
|
|
283
|
+
} catch (err) {
|
|
284
|
+
const status = err.status || null;
|
|
285
|
+
return [
|
|
286
|
+
{
|
|
287
|
+
check: 'auth_methods',
|
|
288
|
+
success: false,
|
|
289
|
+
details: {
|
|
290
|
+
server_url: runtime.url,
|
|
291
|
+
origin: resolvedOrigin,
|
|
292
|
+
status,
|
|
293
|
+
code: err.code || 'AUTH_METHODS_FAILED',
|
|
294
|
+
error: err.message || String(err),
|
|
295
|
+
},
|
|
296
|
+
fix:
|
|
297
|
+
status === 401 || status === 403
|
|
298
|
+
? 'Set SPAPS_API_KEY and SPAPS_ORIGIN (or pass --origin) for the application whose auth methods you are diagnosing.'
|
|
299
|
+
: 'Ensure GET /api/auth/methods is mounted and reachable on the configured SPAPS server.',
|
|
300
|
+
},
|
|
301
|
+
];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
227
305
|
function formatHuman(results) {
|
|
228
306
|
const ok = results.every(r => r.success);
|
|
229
307
|
console.log(chalk.yellow('\nš SPAPS Doctor\n'));
|
|
230
308
|
results.forEach(r => {
|
|
231
309
|
const icon = r.success ? chalk.green('ā') : chalk.red('ā');
|
|
232
310
|
console.log(`${icon} ${r.check} ${chalk.gray(JSON.stringify(r.details))}`);
|
|
311
|
+
if (r.check === 'auth_methods' && Array.isArray(r.details?.methods)) {
|
|
312
|
+
r.details.methods.forEach((method) => {
|
|
313
|
+
const state = method.enabled ? chalk.green('enabled') : chalk.gray('disabled');
|
|
314
|
+
console.log(` ${method.method}: ${state}`);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
233
317
|
if (!r.success && r.fix) console.log(chalk.cyan(` fix: ${r.fix}`));
|
|
234
318
|
});
|
|
235
319
|
console.log();
|
|
236
320
|
console.log(ok ? chalk.green('All checks passed!') : chalk.red('Some checks failed. See fixes above.'));
|
|
237
321
|
}
|
|
238
322
|
|
|
239
|
-
async function runDoctor({
|
|
323
|
+
async function runDoctor({
|
|
324
|
+
port = DEFAULT_PORT,
|
|
325
|
+
serverUrl = null,
|
|
326
|
+
stripe = null,
|
|
327
|
+
json = false,
|
|
328
|
+
origin = null,
|
|
329
|
+
} = {}) {
|
|
240
330
|
const results = [];
|
|
241
331
|
results.push(checkNodeVersion());
|
|
242
|
-
results.push(await checkPort(port));
|
|
332
|
+
results.push(await checkPort(port, serverUrl));
|
|
243
333
|
// Warn if using 3000 which often collides with Next.js
|
|
244
334
|
if (port === 3000) {
|
|
245
335
|
results.push({
|
|
@@ -260,6 +350,8 @@ async function runDoctor({ port = DEFAULT_PORT, stripe = null, json = false } =
|
|
|
260
350
|
results.push(await checkWebhook(port));
|
|
261
351
|
const domainResults = await checkDomainMounts(port);
|
|
262
352
|
results.push(...domainResults);
|
|
353
|
+
const authResults = await checkAuthSurface({ port, serverUrl, origin });
|
|
354
|
+
results.push(...authResults);
|
|
263
355
|
|
|
264
356
|
const ok = results.every(r => r.success);
|
|
265
357
|
const payload = { success: ok, results, next_steps: ok ? [] : ['Apply suggested fixes and re-run: npx spaps doctor --json'] };
|
|
@@ -271,4 +363,4 @@ async function runDoctor({ port = DEFAULT_PORT, stripe = null, json = false } =
|
|
|
271
363
|
return payload;
|
|
272
364
|
}
|
|
273
365
|
|
|
274
|
-
module.exports = { runDoctor, checkDomainMounts };
|
|
366
|
+
module.exports = { runDoctor, checkDomainMounts, checkAuthSurface };
|